Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/theme/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

### Documentation

- Document design token accessibility responsibilities ([#79943](https://github.com/WordPress/gutenberg/pull/79943)).
- Document that `ThemeProvider` does not accept wrapper customization props ([#79763](https://github.com/WordPress/gutenberg/pull/79763)).
- Clarify the design token documentation entry points and keep the generated token guidance source internal ([#79829](https://github.com/WordPress/gutenberg/pull/79829)).
- Document intentional design token omissions in the design system tokens reference ([#79770](https://github.com/WordPress/gutenberg/pull/79770)).
Expand Down
21 changes: 21 additions & 0 deletions packages/theme/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,27 @@ For the best development experience, we recommend configuring the [Stylelint rul

If you reference `--wpds-*` tokens in CSS or JS/TS source, use the [build plugins](#build-plugins) to inject fallback values at build time so components render correctly even when the tokens stylesheet is not loaded. If you use `@wordpress/build`, these plugins are already enabled by default when `@wordpress/theme` is installed.

### Accessibility

The semantic color tokens are designed so the default foreground/background pairs used by the design system meet [WCAG AA text contrast](https://www.w3.org/WAI/WCAG22/Understanding/contrast-minimum.html). Components still need to choose the correct semantic pair for the UI state they render, and must not rely on color alone to convey information.

Design tokens do not replace component-level accessibility handling:

- **Forced colors:** components are still responsible for `forced-colors` overrides where native high-contrast rendering would otherwise hide borders, icons, focus rings, or state indicators.
- **Reduced motion:** motion tokens provide shared durations and easing curves, but consuming components are still responsible for respecting `prefers-reduced-motion` for non-essential animations.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WDYT about adding a code example here to educate folks better? Not everyone knows how to do this or cares to seek it, but if we just show it, it might become a habit.

@media (prefers-reduced-motion: no-preference) {
	.example {
		transition: opacity var(--wpds-motion-duration-md) var(--wpds-motion-easing-subtle);
	}
}

or

@media not (prefers-reduced-motion) {
	.example {
		transition: opacity var(--wpds-motion-duration-md) var(--wpds-motion-easing-subtle);
	}
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Example added


For example, define non-essential transitions only when the user has not requested reduced motion:

```css
@media not ( prefers-reduced-motion ) {
.example {
transition-property: opacity;
transition-duration: var( --wpds-motion-duration-md );
transition-timing-function: var( --wpds-motion-easing-subtle );
}
}
```

## Theme Provider

The `ThemeProvider` is a React component that should wrap your application to provide design tokens and theme context to the child UI components. It accepts a set of customizable seed values and automatically generates a set of design tokens, which are exposed as CSS custom properties for use throughout the application.
Expand Down
141 changes: 141 additions & 0 deletions packages/theme/src/test/semantic-color-contrast.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import { renderHook } from '@testing-library/react';
import { getContrast } from '../color-ramps/lib/color-utils';
import { useThemeProviderStyles } from '../use-theme-provider-styles';

const MINIMUM_TEXT_CONTRAST = 4.5;
const CUSTOM_PRIMARY = '#0057b8';
const CUSTOM_BACKGROUND = '#f6f3ef';

const THEME_PROVIDER_STYLE_CASES = [
{
name: 'default seed colors',
settings: undefined,
},
{
name: 'custom seed colors',
settings: {
color: {
primary: CUSTOM_PRIMARY,
background: CUSTOM_BACKGROUND,
},
},
},
] as const;

const CONTRAST_PAIRS = [
{
background: '--wpds-color-background-surface-neutral',
foreground: '--wpds-color-foreground-content-neutral',
},
{
background: '--wpds-color-background-surface-neutral-strong',
foreground: '--wpds-color-foreground-content-neutral',
},
{
background: '--wpds-color-background-surface-neutral-weak',
foreground: '--wpds-color-foreground-content-neutral',
},
{
background: '--wpds-color-background-surface-neutral',
foreground: '--wpds-color-foreground-content-neutral-weak',
},
{
background: '--wpds-color-background-surface-info',
foreground: '--wpds-color-foreground-content-info',
},
{
background: '--wpds-color-background-surface-info-weak',
foreground: '--wpds-color-foreground-content-info-weak',
},
{
background: '--wpds-color-background-surface-success',
foreground: '--wpds-color-foreground-content-success',
},
{
background: '--wpds-color-background-surface-success-weak',
foreground: '--wpds-color-foreground-content-success-weak',
},
{
background: '--wpds-color-background-surface-warning',
foreground: '--wpds-color-foreground-content-warning',
},
{
background: '--wpds-color-background-surface-warning-weak',
foreground: '--wpds-color-foreground-content-warning-weak',
},
{
background: '--wpds-color-background-surface-caution',
foreground: '--wpds-color-foreground-content-caution',
},
{
background: '--wpds-color-background-surface-caution-weak',
foreground: '--wpds-color-foreground-content-caution-weak',
},
{
background: '--wpds-color-background-surface-error',
foreground: '--wpds-color-foreground-content-error',
},
{
background: '--wpds-color-background-surface-error-weak',
foreground: '--wpds-color-foreground-content-error-weak',
},
{
background: '--wpds-color-background-interactive-brand-strong',
foreground: '--wpds-color-foreground-interactive-brand-strong',
},
{
background: '--wpds-color-background-interactive-brand-strong-active',
foreground: '--wpds-color-foreground-interactive-brand-strong-active',
},
{
background: '--wpds-color-background-interactive-error-strong',
foreground: '--wpds-color-foreground-interactive-error-strong',
},
{
background: '--wpds-color-background-interactive-error-strong-active',
foreground: '--wpds-color-foreground-interactive-error-strong-active',
},
{
background: '--wpds-color-background-interactive-neutral-strong',
foreground: '--wpds-color-foreground-interactive-neutral-strong',
},
{
background: '--wpds-color-background-interactive-neutral-strong-active',
foreground: '--wpds-color-foreground-interactive-neutral-strong-active',
},
] as const;

function readToken(
styles: Record< string, string | number | undefined >,
token: string
) {
const value = styles[ token ];
if ( typeof value !== 'string' || value === '' ) {
throw new Error( `Missing semantic color token: ${ token }` );
}
return value;
}

describe( 'semantic color contrast', () => {
it.each( THEME_PROVIDER_STYLE_CASES )(
'keeps critical foreground/background pairs above WCAG AA text contrast with $name',
( { settings } ) => {
const { result } = renderHook( () =>
useThemeProviderStyles( settings )
);
const styles = result.current.themeProviderStyles as Record<
string,
string | number | undefined
>;

CONTRAST_PAIRS.forEach( ( { foreground, background } ) => {
const foregroundValue = readToken( styles, foreground );
const backgroundValue = readToken( styles, background );

expect(
getContrast( foregroundValue, backgroundValue )
).toBeGreaterThanOrEqual( MINIMUM_TEXT_CONTRAST );
} );
}
);
} );
Loading