diff --git a/modules/docs/llm/upgrade-guides/15.0-UPGRADE-GUIDE.md b/modules/docs/llm/upgrade-guides/15.0-UPGRADE-GUIDE.md index 2062da6b6f..367edf2f54 100644 --- a/modules/docs/llm/upgrade-guides/15.0-UPGRADE-GUIDE.md +++ b/modules/docs/llm/upgrade-guides/15.0-UPGRADE-GUIDE.md @@ -44,6 +44,8 @@ semantic meaning to allow for better use and theming. - [Indicators](#indicators) - [Navigation](#navigation) - [Popups](#popups) +- [Theming](#theming) + - [System Brand Tokens and Brand Tokens](#system-brand-tokens-and-brand-tokens) - [Deprecations](#deprecations) - [Accent Icon](#accent-icon) - [Applet Icon](#applet-icon) @@ -537,6 +539,23 @@ If you'd like to see the visual differences between `v14` with `v3` tokens and ` `Menu`, `Modal`, `Popup`, `Toast` and `Tooltip` +## Theming + +### System Brand Tokens and Brand Tokens + +The relationship between **system brand tokens** (e.g. `system.color.brand.accent.primary`) and **brand tokens** (e.g. `brand.primary600`) has changed. Teams can still set palette values such as `base`, `light`, `lighter`, `lightest`, `dark` and `darkest` via the `CanvasProvider` `theme` prop. The mapping inside `CanvasProvider` exists for **backwards compatibility**. When you pass a theme object, we forward those values to both the legacy brand tokens and the system brand tokens so current implementations will continue to work. + +For more information on theming, view our [Theming](https://workday.github.io/canvas-kit/?path=/docs/features-theming-overview--docs) documentation. + +For more information on our tokens, vie\w our [Tokens](https://workday.github.io/canvas-tokens/?path=/docs/docs-getting-started--docs) documentation. + +```tsx +// This will set the [brand.primary.**] tokens to shades of purple. + + + +``` + ## Deprecations We add the [@deprecated](https://jsdoc.app/tags-deprecated.html) JSDoc tag to code we plan to remove diff --git a/modules/docs/mdx/15.0-UPGRADE-GUIDE.mdx b/modules/docs/mdx/15.0-UPGRADE-GUIDE.mdx index bc11d74161..c18cc5ee60 100644 --- a/modules/docs/mdx/15.0-UPGRADE-GUIDE.mdx +++ b/modules/docs/mdx/15.0-UPGRADE-GUIDE.mdx @@ -48,6 +48,8 @@ semantic meaning to allow for better use and theming. - [Indicators](#indicators) - [Navigation](#navigation) - [Popups](#popups) +- [Theming](#theming) + - [System Brand Tokens and Brand Tokens](#system-brand-tokens-and-brand-tokens) - [Deprecations](#deprecations) - [Accent Icon](#accent-icon) - [Applet Icon](#applet-icon) @@ -544,6 +546,24 @@ If you'd like to see the visual differences between `v14` with `v3` tokens and ` `Menu`, `Modal`, `Popup`, `Toast` and `Tooltip` +## Theming + +### System Brand Tokens and Brand Tokens + +The relationship between **system brand tokens** (e.g. `system.color.brand.accent.primary`) and **brand tokens** (e.g. `brand.primary600`) has changed. Teams can still set palette values such as `base`, `light`, `lighter`, `lightest`, `dark` and `darkest` via the `CanvasProvider` `theme` prop. The mapping inside `CanvasProvider` exists for **backwards compatibility**. When you pass a theme object, we forward those values to both the legacy brand tokens and the system brand tokens so current implementations will continue to work. + +For more information on theming, view our [Theming](https://workday.github.io/canvas-kit/?path=/docs/features-theming-overview--docs) documentation. + +For more information on our tokens, vie\w our [Tokens](https://workday.github.io/canvas-tokens/?path=/docs/docs-getting-started--docs) documentation. + +```tsx +// This will set the [brand.primary.**] tokens to shades of purple. + + + +``` + + ## Deprecations We add the [@deprecated](https://jsdoc.app/tags-deprecated.html) JSDoc tag to code we plan to remove diff --git a/modules/react/common/lib/CanvasProvider.tsx b/modules/react/common/lib/CanvasProvider.tsx index 926cc61df6..fe77c45324 100644 --- a/modules/react/common/lib/CanvasProvider.tsx +++ b/modules/react/common/lib/CanvasProvider.tsx @@ -2,7 +2,7 @@ import {CacheProvider, Theme, ThemeProvider} from '@emotion/react'; import * as React from 'react'; import {createStyles, getCache, maybeWrapCSSVariables} from '@workday/canvas-kit-styling'; -import {base, brand} from '@workday/canvas-tokens-web'; +import {base, brand, system} from '@workday/canvas-tokens-web'; import {PartialEmotionCanvasTheme, defaultCanvasTheme, useTheme} from './theming'; @@ -26,6 +26,92 @@ const mappedKeys = { contrast: 'accent', }; +/** + * Mapping from deprecated theme palette keys to new numerical brand tokens. + * This ensures backwards compatibility when consumers use the old theme format. + * For example: palette.primary.main -> brand.primary600 + */ +const numericalTokenMapping = { + lightest: '25', + lighter: '50', + light: '200', + main: '600', + dark: '700', + darkest: '800', +} as const; + +/** + * Mapping from deprecated theme palette colors to new brand token names. + * For example: + * `primary` -> `primary` + * `error` -> `critical` + * `success` -> `positive` + * `alert` -> `caution` + * `neutral` -> `neutral` + */ +const brandColorMapping = { + primary: 'primary', + error: 'critical', + success: 'positive', + alert: 'caution', + neutral: 'neutral', +} as const; + +/** + * Mapping from deprecated common palette keys to new brand.common tokens. + * + * ## Brandable System Tokens + * + * These are all the `system.color.brand.*` tokens that can be customized via theming. + * Each token references a brand token that can be overridden through the CanvasProvider theme prop. + * + * ### Focus Tokens + * - `system.color.brand.focus.primary` → `brand.primary.500` → Controlled by `focusOutline` (separately from `palette.primary.main`, which controls `brand.primary.600`) + * - `system.color.brand.focus.critical` → `brand.critical.500` → Controlled by `palette.error.dark` or `errorInner` + * - `system.color.brand.focus.caution.inner` → `brand.caution.400` → Controlled by `palette.alert.main` or `alertInner` + * - `system.color.brand.focus.caution.outer` → `brand.caution.500` → Controlled by `palette.alert.dark` or `alertOuter` + * + * ### Border Tokens + * - `system.color.brand.border.primary` → `brand.primary.500` → Controlled by `focusOutline` (separately from `palette.primary.main`, which controls `brand.primary.600`) + * - `system.color.brand.border.critical` → `brand.critical.500` → Controlled by `palette.error.dark` or `errorInner` + * - `system.color.brand.border.caution` → `brand.caution.500` → Controlled by `palette.alert.dark` or `alertOuter` + * + * ### Surface Tokens + * - `system.color.brand.surface.primary.default` → `brand.primary.A25` → Controlled by `palette.primary.lightest` + * - `system.color.brand.surface.primary.strong` → `brand.primary.A50` → Controlled by `palette.primary.lighter` + * - `system.color.brand.surface.critical.default` → `brand.critical.A25` → Controlled by `palette.error.lightest` + * - `system.color.brand.surface.critical.strong` → `brand.critical.A50` → Controlled by `palette.error.lighter` + * - `system.color.brand.surface.caution.default` → `brand.caution.A25` → Controlled by `palette.alert.lightest` + * - `system.color.brand.surface.caution.strong` → `brand.caution.A50` → Controlled by `palette.alert.lighter` + * - `system.color.brand.surface.positive.default` → `brand.positive.A25` → Controlled by `palette.success.lightest` + * - `system.color.brand.surface.positive.strong` → `brand.positive.A50` → Controlled by `palette.success.lighter` + * - `system.color.brand.surface.selected` → `brand.primary.A50` → Controlled by `palette.primary.lighter` + * + * ### Accent Tokens + * - `system.color.brand.accent.primary` → `brand.primary.600` → Controlled by `palette.primary.main` + * - `system.color.brand.accent.critical` → `brand.critical.600` → Controlled by `palette.error.main` + * - `system.color.brand.accent.caution` → `brand.caution.400` → Controlled by `palette.alert.main` + * - `system.color.brand.accent.positive` → `brand.positive.600` → Controlled by `palette.success.main` + * - `system.color.brand.accent.action` → `brand.primary.600` → Controlled by `palette.primary.main` + * + * ### Foreground (Text/Icon) Tokens + * - `system.color.brand.fg.primary.default` → `brand.primary.600` → Controlled by `palette.primary.main` + * - `system.color.brand.fg.primary.strong` → `brand.primary.700` → Controlled by `palette.primary.dark` + * - `system.color.brand.fg.critical.default` → `brand.critical.600` → Controlled by `palette.error.main` + * - `system.color.brand.fg.critical.strong` → `brand.critical.700` → Controlled by `palette.error.dark` + * - `system.color.brand.fg.caution.default` → `brand.caution.600` → Controlled by `palette.alert.darkest` + * - `system.color.brand.fg.caution.strong` → `brand.caution.700` → Controlled by `palette.alert.dark` (Note: no direct mapping, inherits default) + * - `system.color.brand.fg.positive.default` → `brand.positive.600` → Controlled by `palette.success.main` + * - `system.color.brand.fg.positive.strong` → `brand.positive.700` → Controlled by `palette.success.dark` + * - `system.color.brand.fg.selected` → `brand.primary.700` → Controlled by `palette.primary.dark` + */ +const commonTokenMapping = { + focusOutline: brand.common.focus, // maps to brand.primary500 + alertInner: brand.common.caution.inner, // maps to brand.caution400 + alertOuter: brand.common.caution.outer, // maps to brand.caution500 + errorInner: brand.common.critical, // maps to brand.critical500 +} as const; + /** * If you wish to reset the theme to the default, apply this class on the CanvasProvider. */ @@ -84,32 +170,208 @@ export const useCanvasThemeToCssVars = ( const className = (elemProps.className || '').split(' ').concat(defaultBranding).join(' '); const style = elemProps.style || {}; const {palette} = filledTheme.canvas; + (['common', 'primary', 'error', 'alert', 'success', 'neutral'] as const).forEach(color => { if (color === 'common') { (['focusOutline', 'alertInner', 'alertOuter', 'errorInner'] as const).forEach(key => { if (palette.common[key] !== defaultCanvasTheme.palette.common[key]) { + const value = maybeWrapCSSVariables(palette.common[key]); + + // Set deprecated token for backwards compatibility //@ts-ignore - style[brand.common.focusOutline] = maybeWrapCSSVariables(palette.common.focusOutline); - //@ts-ignore - style[brand.common.alertInner] = maybeWrapCSSVariables(palette.common.alertInner); - //@ts-ignore - style[brand.common.alertOuter] = maybeWrapCSSVariables(palette.common.alertOuter); + style[brand.common[key]] = value; + + // Forward to new brand.common tokens //@ts-ignore - style[brand.common.errorInner] = maybeWrapCSSVariables(palette.common.errorInner); + style[commonTokenMapping[key]] = value; + + // Additional system token forwarding for focusOutline + if (key === 'focusOutline') { + // system.color.brand.focus.primary -> brand.primary.500 (via brand.common.focus) + // @ts-ignore + style[system.color.brand.focus.primary] = value; + // system.color.brand.border.primary -> brand.primary.500 + // @ts-ignore + style[system.color.brand.border.primary] = value; + } + + // Additional system token forwarding for alertInner + if (key === 'alertInner') { + // Forward alertInner to system.color.brand.focus.caution.inner + // This token is used by components (e.g., TextInput with error="caution") + // for inner focus ring styling. Maps to brand.caution400 via brand.common.caution.inner. + // This ensures backwards compatibility when users customize alertInner in their theme. + // @ts-ignore + style[system.color.brand.focus.caution.inner] = value; + } + + // Additional system token forwarding for alertOuter + if (key === 'alertOuter') { + // Forward alertOuter to system.color.brand.border.caution + // This token is used by components (e.g., TextInput with error="caution") + // for border and outer focus ring styling. Maps to brand.caution500 via brand.common.caution.outer. + // This ensures backwards compatibility when users customize alertOuter in their theme. + // @ts-ignore + style[system.color.brand.border.caution] = value; + } + + // Additional system token forwarding for errorInner + if (key === 'errorInner') { + // Forward errorInner to system.color.brand.focus.critical and system.color.brand.border.critical + // These tokens are used by components (e.g., TextInput with error="error", Switch) for critical + // focus ring and border styling. Maps to brand.critical500 via brand.common.errorInner. + // This ensures backwards compatibility when users customize errorInner in their theme. + // @ts-ignore + style[system.color.brand.focus.critical] = value; + // @ts-ignore + style[system.color.brand.border.critical] = value; + } } }); - } - (['lightest', 'lighter', 'light', 'main', 'dark', 'darkest', 'contrast'] as const).forEach( - key => { - // We only want to set custom colors if they do not match the default. The `defaultBranding` class will take care of the rest. - //@ts-ignore - if (palette[color][key] !== defaultCanvasTheme.palette[color][key]) { - // @ts-ignore - style[brand[color][mappedKeys[key]]] = maybeWrapCSSVariables(palette[color][key]); + } else { + (['lightest', 'lighter', 'light', 'main', 'dark', 'darkest', 'contrast'] as const).forEach( + key => { + // We only want to set custom colors if they do not match the default. The `defaultBranding` class will take care of the rest. + //@ts-ignore + if (palette[color][key] !== defaultCanvasTheme.palette[color][key]) { + const value = maybeWrapCSSVariables(palette[color][key]); + + // Set deprecated token (e.g., brand.primary.base) for backwards compatibility + // @ts-ignore + style[brand[color][mappedKeys[key]]] = value; + + // Forward to new numerical brand tokens (e.g., brand.primary600) + // Skip 'contrast' as it doesn't map to numerical tokens + if (key !== 'contrast' && key in numericalTokenMapping) { + const newBrandColor = brandColorMapping[color as keyof typeof brandColorMapping]; + const numericalSuffix = + numericalTokenMapping[key as keyof typeof numericalTokenMapping]; + + // @ts-ignore - Dynamically access brand tokens like brand.primary600 + const numericalToken = brand[newBrandColor + numericalSuffix]; + if (numericalToken) { + // @ts-ignore + style[numericalToken] = value; + } + + // Forward to all relevant system.color.brand.* tokens + // These system tokens reference the numerical brand tokens, so updating them ensures full compatibility + if (key === 'main') { + // system.color.brand.accent.{color} -> brand.{color}.600 (except caution -> 400) + // @ts-ignore + const systemAccentToken = system.color.brand.accent[newBrandColor]; + if (systemAccentToken) { + // @ts-ignore + style[systemAccentToken] = value; + } + + // system.color.brand.fg.{color}.default -> brand.{color}.600 + // @ts-ignore + const systemFgToken = system.color.brand.fg[newBrandColor]?.default; + if (systemFgToken) { + // @ts-ignore + style[systemFgToken] = value; + } + + // system.color.brand.focus.primary (maps to brand.primary.500 per docs) + // For primary only, update focus when 'main' changes — unless focusOutline was customized (it takes precedence) + if (newBrandColor === 'primary') { + const focusOutlineCustomized = + palette.common.focusOutline !== defaultCanvasTheme.palette.common.focusOutline; + if (!focusOutlineCustomized) { + // @ts-ignore + const focusToken = system.color.brand.focus.primary; + if (focusToken) { + // @ts-ignore + style[focusToken] = value; + } + } + } + } else if (key === 'dark') { + // system.color.brand.fg.{color}.strong -> brand.{color}.700 + // @ts-ignore + const systemFgStrongToken = system.color.brand.fg[newBrandColor]?.strong; + if (systemFgStrongToken) { + // @ts-ignore + style[systemFgStrongToken] = value; + } + + // system.color.brand.fg.selected -> brand.primary.700 (for primary only) + if (newBrandColor === 'primary') { + // @ts-ignore + const selectedToken = system.color.brand.fg.selected; + if (selectedToken) { + // @ts-ignore + style[selectedToken] = value; + } + } + + // system.color.brand.focus.critical & system.color.brand.border.critical -> brand.critical.500 (palette.error.dark) + if (newBrandColor === 'critical') { + // @ts-ignore + const focusCriticalToken = system.color.brand.focus.critical; + if (focusCriticalToken) { + // @ts-ignore + style[focusCriticalToken] = value; + } + // @ts-ignore + const borderCriticalToken = system.color.brand.border.critical; + if (borderCriticalToken) { + // @ts-ignore + style[borderCriticalToken] = value; + } + } + + // system.color.brand.focus.caution.outer & system.color.brand.border.caution -> brand.caution.500 (palette.alert.dark) + if (newBrandColor === 'caution') { + // @ts-ignore + const focusCautionOuterToken = system.color.brand.focus.caution?.outer; + if (focusCautionOuterToken) { + // @ts-ignore + style[focusCautionOuterToken] = value; + } + // @ts-ignore + const borderCautionToken = system.color.brand.border.caution; + if (borderCautionToken) { + // @ts-ignore + style[borderCautionToken] = value; + } + } + } else if (key === 'lighter') { + // system.color.brand.surface.{color}.strong -> brand.{color}.A50 + // Note: A50 tokens are different from regular 50 tokens but we'll forward the lighter value + // @ts-ignore + const surfaceStrongToken = system.color.brand.surface[newBrandColor]?.strong; + if (surfaceStrongToken) { + // @ts-ignore + style[surfaceStrongToken] = value; + } + + // system.color.brand.surface.selected -> brand.primary.A50 (for primary only) + if (newBrandColor === 'primary') { + // @ts-ignore + const selectedSurfaceToken = system.color.brand.surface.selected; + if (selectedSurfaceToken) { + // @ts-ignore + style[selectedSurfaceToken] = value; + } + } + } else if (key === 'lightest') { + // system.color.brand.surface.{color}.default -> brand.{color}.A25 + // @ts-ignore + const surfaceDefaultToken = system.color.brand.surface[newBrandColor]?.default; + if (surfaceDefaultToken) { + // @ts-ignore + style[surfaceDefaultToken] = value; + } + } + } + } } - } - ); + ); + } }); + return {...elemProps, className, style}; }; diff --git a/modules/react/common/stories/mdx/Theming.mdx b/modules/react/common/stories/mdx/Theming.mdx index f392336eb3..83a7822b76 100644 --- a/modules/react/common/stories/mdx/Theming.mdx +++ b/modules/react/common/stories/mdx/Theming.mdx @@ -9,7 +9,7 @@ import {Theming} from './examples/Theming'; ## Overview -Canvas Kit v14 introduces a significant shift in our approach to theming: we've moved away from +Canvas Kit v14 and v15 introduce a significant shift in our approach to theming: we've moved away from JavaScript-based theme objects to CSS variables. This change provides better performance, improved developer experience, and greater flexibility for theming applications. @@ -20,6 +20,8 @@ developer experience, and greater flexibility for theming applications. > > If your application renders within an environment that already imports these CSS variables, **do not re-import them**. +View our latest tokens documentation [here](https://workday.github.io/canvas-tokens/?path=/docs/docs-getting-started--docs). + ## Migration from v10 Theme Prop to v14 CSS Variables ### The Evolution @@ -80,11 +82,11 @@ and scoped to the `div` that the `CanvasProvider` created. This meant that anyth or outside of the `CanvasProvider` would not be able to cascade down to the components within the `CanvasProvider`. -If you provide a `theme` to the `CanvasProvider`, it will create a scoped theme. Note that in v14, global CSS variables are the recommended way to theme Popups and Modals consistently. +If you provide a `theme` to the `CanvasProvider`, it will create a scoped theme. Note that in v14 and v15, global CSS variables are the recommended way to theme Popups and Modals consistently. ## Global vs Scoped Theming -Canvas Kit v14 supports two theming strategies: **global theming** and **scoped theming**. Understanding the difference is important to avoid unexpected behavior. +Canvas Kit v14 and v15 support two theming strategies: **global theming** and **scoped theming**. Understanding the difference is important to avoid unexpected behavior. ### Global Theming @@ -94,16 +96,16 @@ Global theming applies CSS variables at the `:root` level, making them available @import '@workday/canvas-tokens-web/css/base/_variables.css'; :root { // This is showing how you can change the value of a token at the root level of your application. - --cnvs-brand-primary-base: var(--cnvs-base-palette-magenta-600); + --cnvs-brand-primary-600: var(--cnvs-base-palette-magenta-600); } ``` ### Scoped Theming -Scoped theming applies CSS variables to a specific section of your application using the `CanvasProvider` with either a `className` or `theme` prop. The theme only affects components within that provider. +Scoped theming applies CSS variables to a specific section of your application using the `CanvasProvider` via the `theme` prop. The theme only affects components within that provider. ```tsx -// Using the theme prop for scoped theming. This will set the [brand.primary.**] tokens to shades of purple. +// Using the theme prop for scoped theming. This will set the [brand.primary.**] tokens to shades of purple. This will also ensure that the Popup and Modal components are themed consistently. @@ -123,7 +125,7 @@ For all other cases, use global theming at `:root` to ensure consistent theming ## ✅ Preferred Approach (v14+) -Canvas Kit v14 promotes using CSS variables for theming, which can be applied in two ways: +Canvas Kit v14 and v15 promote using CSS variables for theming, which can be applied in two ways: ### Method 1: Global CSS Variables (Recommended) @@ -138,13 +140,12 @@ CSS: :root { /* Override brand primary colors */ - --cnvs-brand-primary-base: var(--cnvs-base-palette-magenta-600); - --cnvs-brand-primary-light: var(--cnvs-base-palette-magenta-200); - --cnvs-brand-primary-lighter: var(--cnvs-base-palette-magenta-50); - --cnvs-brand-primary-lightest: var(--cnvs-base-palette-magenta-25); - --cnvs-brand-primary-dark: var(--cnvs-base-palette-magenta-700); - --cnvs-brand-primary-darkest: var(--cnvs-base-palette-magenta-800); - --cnvs-brand-primary-accent: var(--cnvs-base-palette-neutral-0); + --cnvs-brand-primary-600: var(--cnvs-base-palette-magenta-600); + --cnvs-brand-primary-200: var(--cnvs-base-palette-magenta-200); + --cnvs-brand-primary-50: var(--cnvs-base-palette-magenta-50); + --cnvs-brand-primary-25: var(--cnvs-base-palette-magenta-25); + --cnvs-brand-primary-700: var(--cnvs-base-palette-magenta-700); + --cnvs-brand-primary-800: var(--cnvs-base-palette-magenta-800); } ``` @@ -154,65 +155,18 @@ CSS: ### Method 2: Provider-Level CSS Variables -Use Canvas Kit's `createStyles` utility to generate themed class names that can be applied to +Use Canvas Kit's `CanvasProvider` and `theme` prop to generate themed class names that can be applied to specific components or sections: -> **Note:** Doing the following **will create a cascade barrier**. Only use this method if you -> intentionally want to override the default theme. ```tsx -import {createStyles}from "@workday/canvas-kit-styling" -import {brand, base} from "@workday/canvas-tokens-web" import {CanvasProvider} from "@workday/canvas-kit-react/common" - -// You can import the CSS variables in a ts file or an index.css file. You do not need to do both. -import '@workday/canvas-tokens-web/css/base/_variables.css'; -import '@workday/canvas-tokens-web/css/system/_variables.css'; -import '@workday/canvas-tokens-web/css/brand/_variables.css'; - -// Generate a class name that defines CSS variables -const themedBrand = createStyles({ - [brand.primary.accent]: base.neutral0, - [brand.primary.darkest]: base.blue800, - [brand.primary.dark]: base.blue700, - [brand.primary.base]: base.blue600, - [brand.primary.light]: base.blue200, - [brand.primary.lighter]: base.blue50, - [brand.primary.lightest]: base.blue25, -}) - - +// This will set the [brand.primary.**] tokens to shades of purple. This will also ensure that the Popup and Modal components are themed consistently. + ``` -### Theming Modals and Dialogs - -Previously, the `usePopupStack` hook created a CSS class name that was passed to our Popups. We attached those theme styles to that class name. This allowed the theme to be available in our Popups. But it also created a cascade barrier that blocked the global theme from being applied to our Popup components. -Because we now use global CSS variables, we no longer need this class name to provide the global theme to Popups. But we have to remove this generated class name to allow the global theme to be applied to Popups. - -**Before in v13** - -```tsx -// When passing a theme to the Canvas Provider, the `usePopupStack` would grab the theme and generate a class to forward the theme to Modals and Dialogs. This would create a cascade barrier for any CSS variables defined at the root. - - //... rest of modal code - -``` - -**After in v14** - -```tsx -// If you wish to still theme you application and Modals, you can either define the CSS variables at the root level of your application or define a className and pass it to the CanvasProvider. -:root { - --cnvs-brand-primary-base: blue; -} - - - //... rest of modal code - -``` - ## CSS Token Structure Canvas Kit provides three layers of CSS variables. @@ -233,10 +187,12 @@ Base tokens define foundation palette and design values. Brand tokens define semantic color assignments. ```css ---cnvs-brand-primary-base: var(--cnvs-base-palette-blue-600); ---cnvs-brand-primary-accent: var(--cnvs-base-palette-neutral-0); ---cnvs-brand-error-base: var(--cnvs-base-palette-red-600); ---cnvs-brand-success-base: var(--cnvs-base-palette-green-600); +--cnvs-brand-primary-600: var(--cnvs-base-palette-blue-600); +--cnvs-brand-primary-200: var(--cnvs-base-palette-blue-200); +--cnvs-brand-primary-50: var(--cnvs-base-palette-blue-50); +--cnvs-brand-primary-25: var(--cnvs-base-palette-blue-25); +--cnvs-brand-primary-700: var(--cnvs-base-palette-blue-700); +--cnvs-brand-primary-800: var(--cnvs-base-palette-blue-800); ``` ### System Tokens (`system/_variables.css`) @@ -244,9 +200,8 @@ Brand tokens define semantic color assignments. System tokens define component-specific values. ```css ---cnvs-sys-color-bg-primary-default: var(--cnvs-base-palette-blue-600); ---cnvs-sys-color-text-primary-default: var(--cnvs-base-palette-blue-600); ---cnvs-sys-space-x4: calc(var(--cnvs-base-unit) * 4); +--cnvs-sys-color-bg-default: var(--cnvs-base-palette-blue-600); +--cnvs-sys-shape-sm: var(--cnvs-base-size-50); ``` ## Practical Examples @@ -261,29 +216,19 @@ System tokens define component-specific values. :root { /* Primary brand colors */ - --cnvs-brand-primary-base: var(--cnvs-base-palette-magenta-600); - --cnvs-brand-primary-light: var(--cnvs-base-palette-magenta-200); - --cnvs-brand-primary-lighter: var(--cnvs-base-palette-magenta-50); - --cnvs-brand-primary-lightest: var(--cnvs-base-palette-magenta-25); - --cnvs-brand-primary-dark: var(--cnvs-base-palette-magenta-700); - --cnvs-brand-primary-darkest: var(--cnvs-base-palette-magenta-800); - --cnvs-brand-primary-accent: var(--cnvs-base-palette-neutral-0); + --cnvs-brand-primary-600: var(--cnvs-base-palette-magenta-600); + --cnvs-brand-primary-200: var(--cnvs-base-palette-magenta-200); + --cnvs-brand-primary-50: var(--cnvs-base-palette-magenta-50); + --cnvs-brand-primary-25: var(--cnvs-base-palette-magenta-25); + --cnvs-brand-primary-700: var(--cnvs-base-palette-magenta-700); + --cnvs-brand-primary-800: var(--cnvs-base-palette-magenta-800); } ``` - +### Scoped Theming -### Dark Mode Implementation + -```css -/* Dark mode theming */ -[data-theme='dark'] { - --cnvs-sys-color-bg-default: var(--cnvs-base-palette-neutral-950); - --cnvs-sys-color-text-default: var(--cnvs-base-palette-neutral-50); - --cnvs-sys-color-border-container: var(--cnvs-base-palette-slate-700); - --cnvs-sys-color-bg-alt-default: var(--cnvs-base-palette-slate-800); -} -``` ### RTL Support @@ -410,31 +355,11 @@ const theme = { Replace theme-based `CanvasProvider` usage with CSS class-based theming. ```tsx -// Before: - - - - -// After: - + ``` -> **Note:** Using a class means you will need to define each property of the palette for full -> control over theming. - -### Step 4: Test Component Rendering - -Verify that Canvas Kit components (like `PrimaryButton`) correctly use the new CSS variables. - -```tsx -import {PrimaryButton} from '@workday/canvas-kit-react/button'; - -// This should automatically use your CSS variable overrides -Themed Button; -``` - ## Best Practices ### 1. Use Semantic Token Names @@ -443,7 +368,7 @@ Use brand tokens instead of base tokens for better maintainability. ```css /* ✅ Good - semantic meaning */ ---cnvs-brand-primary-base: var(--cnvs-base-palette-blue-600); +--cnvs-brand-primary-600: var(--cnvs-base-palette-blue-600); /* ❌ Avoid - direct base token usage */ --cnvs-base-palette-blue-600: blue; @@ -453,13 +378,7 @@ Use brand tokens instead of base tokens for better maintainability. Ensure color combinations meet accessibility standards. -```css -/* Verify contrast ratios for text/background combinations */ -:root { - --cnvs-brand-primary-base: var(--cnvs-base-palette-blue-600); - --cnvs-brand-primary-accent: var(--cnvs-base-palette-neutral-0); /* White text */ -} -``` +For a full list of color contrast pairs, view our [Color Contrast](https://canvas.workday.com/guidelines/color/color-contrast) documentation. ### 3. Avoid Component Level Theming @@ -469,15 +388,11 @@ component level. ```tsx /* ✅ Good - App level theming */ import {CanvasProvider} from '@workday/canvas-kit-react/common'; -import {createStyles} from '@workday/canvas-kit-styling'; -import {base, brand} from '@workday/canvas-tokens-web'; -const myCustomTheme = createStyles({ - [brand.primary.base]: base.magenta600 -}) +import {base, brand} from '@workday/canvas-tokens-web'; - + @@ -495,18 +410,6 @@ const myCustomTheme = createStyles({ ``` -## Component Compatibility - -All Canvas Kit components in v14 automatically consume CSS variables. No component-level changes are -required when switching from the theme prop approach to CSS variables. - -### Supported Components - -- ✅ All Button variants (`PrimaryButton`, `SecondaryButton`, etc.) -- ✅ Form components (`TextInput`, `FormField`, etc.) -- ✅ Layout components (`Card`, `Modal`, etc.) -- ✅ Navigation components (`Tabs`, `SidePanel`, etc.) - ## Performance Benefits The CSS variable approach provides several performance improvements: diff --git a/modules/react/common/stories/mdx/examples/Theming.tsx b/modules/react/common/stories/mdx/examples/Theming.tsx index d756521d8f..ff2f8b5a4e 100644 --- a/modules/react/common/stories/mdx/examples/Theming.tsx +++ b/modules/react/common/stories/mdx/examples/Theming.tsx @@ -1,17 +1,9 @@ import {PrimaryButton} from '@workday/canvas-kit-react/button'; import {Card} from '@workday/canvas-kit-react/card'; import {CanvasProvider} from '@workday/canvas-kit-react/common'; -import {createStyles} from '@workday/canvas-kit-styling'; -import {base, brand, system} from '@workday/canvas-tokens-web'; - -const customTheme = createStyles({ - [brand.primary.base]: base.green600, - [brand.primary.dark]: base.green700, - [brand.primary.darkest]: base.green800, - [brand.common.focusOutline]: base.green600, - [system.color.fg.strong]: base.indigo900, - [system.color.border.container]: base.indigo300, -}); +import {FormField} from '@workday/canvas-kit-react/form-field'; +import {TextInput} from '@workday/canvas-kit-react/text-input'; +import {base} from '@workday/canvas-tokens-web'; const App = () => { return ( @@ -21,12 +13,15 @@ const App = () => { palette: { primary: { main: base.green600, - dark: base.green700, - darkest: base.green800, - light: base.green200, - lighter: base.green50, - lightest: base.green25, - contrast: base.neutral0, + }, + alert: { + main: base.magenta600, + }, + common: { + focusOutline: base.purple500, + alertInner: base.magenta400, + alertOuter: base.magenta500, + errorInner: base.red500, }, }, }, @@ -36,7 +31,12 @@ const App = () => { Theming Theming - + + Email + + + + @@ -45,8 +45,8 @@ const App = () => { export const Theming = () => { return ( - +
- +
); }; diff --git a/modules/react/popup/lib/hooks/usePopupStack.ts b/modules/react/popup/lib/hooks/usePopupStack.ts index 52b089daa7..19c477dc99 100644 --- a/modules/react/popup/lib/hooks/usePopupStack.ts +++ b/modules/react/popup/lib/hooks/usePopupStack.ts @@ -51,6 +51,7 @@ export const usePopupStack = ( target?: HTMLElement | React.RefObject ): React.RefObject => { const {elementRef, localRef} = useLocalRef(ref); + const theme = React.useContext(ThemeContext as React.Context); const {style} = useCanvasThemeToCssVars(theme, {}); const firstLoadRef = React.useRef(true); // React 19 can call a useState more than once, so we need to track if we've already created a container @@ -65,6 +66,28 @@ export const usePopupStack = ( } return localRef.current; }); + + // Forward only theme overrides (style) to the popup container when a theme was provided via + // CanvasProvider theme prop. We do NOT apply defaultBranding (className) so we don't create a + // cascade barrier—only the CSS variables the consumer overrode are set. This effect runs + // before PopupStack.add below so the container has the theme before it's shown (avoids blue→magenta flash). + React.useLayoutEffect(() => { + const element = localRef.current; + if (!element) { + return undefined; + } + const styleKeys = Object.keys(style); + if (styleKeys.length === 0) { + return undefined; + } + for (const key of styleKeys) { + // @ts-ignore - token keys are CSS custom property names + element.style.setProperty(key, style[key]); + } + // No cleanup: leave theme on container so reopening doesn't flash + return undefined; + }, [localRef, style]); + // We useLayoutEffect to ensure proper timing of registration of the element to the popup stack. // Without this, the timing is unpredictable when mixed with other frameworks. Other frameworks // should also register as soon as the element is available @@ -113,23 +136,5 @@ export const usePopupStack = ( } }, [localRef, target]); - React.useLayoutEffect(() => { - const element = localRef.current; - const keys = Object.keys(style); - if (element && theme) { - for (const key of keys) { - // @ts-ignore - element.style.setProperty(key, style[key]); - } - return () => { - for (const key of keys) { - element.style.removeProperty(key); - } - }; - } - // No cleanup is needed if element or theme is not set, so return undefined (no effect) - return undefined; - }, [localRef, style, theme]); - return localRef; }; diff --git a/modules/react/popup/stories/visual-testing/Popup.stories.tsx b/modules/react/popup/stories/visual-testing/Popup.stories.tsx index 17b1a8320e..e6a2b15a2e 100644 --- a/modules/react/popup/stories/visual-testing/Popup.stories.tsx +++ b/modules/react/popup/stories/visual-testing/Popup.stories.tsx @@ -170,8 +170,8 @@ export const PopupThemed = { }); return ( - Primary Button + Primary Button diff --git a/modules/react/side-panel/spec/SSR.spec.tsx b/modules/react/side-panel/spec/SSR.spec.tsx index b2a70bbc47..39172ee563 100644 --- a/modules/react/side-panel/spec/SSR.spec.tsx +++ b/modules/react/side-panel/spec/SSR.spec.tsx @@ -3,6 +3,7 @@ */ import React from 'react'; import {renderToString} from 'react-dom/server'; + import {SidePanel} from '@workday/canvas-kit-react/side-panel'; describe('SidePanel', () => {