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
3 changes: 3 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions packages/base-styles/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## Unreleased

### Enhancements

- Add `outset-ring__focus` mixin for outline-based focus rings using `--wpds-*` design tokens ([#78698](https://github.com/WordPress/gutenberg/pull/78698)).

## 10.1.0 (2026-06-24)

## 10.0.1 (2026-06-16)
Expand Down
14 changes: 14 additions & 0 deletions packages/base-styles/_mixins.scss
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

@use "sass:color";
@use "sass:math";
@use "@wordpress/theme/utils" as wpds;

@manzoorwanijk manzoorwanijk Jun 26, 2026 •

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.

Heads-up: This import will break wp-build for consumers using isolated dependencies (pnpm etc.).

@wordpress/theme is a transitive dep, so under isolated layouts it isn't hoisted to the root node_modules, i.e. the bare @use then fails with Can't find stylesheet to import. It only works under hoisting by luck.

We may need to apply this change (click to expand)
diff --git a/packages/wp-build/lib/build.mjs b/packages/wp-build/lib/build.mjs
index 448e9ea498c..efa5cc8546c 100755
--- a/packages/wp-build/lib/build.mjs
+++ b/packages/wp-build/lib/build.mjs
@@ -203,6 +203,8 @@ function getSassOptions( workingDir ) {
 			path.join( workingDir, 'node_modules' ),
 			// Root node_modules (for npm hoisted deps)
 			path.join( ROOT_DIR, 'node_modules' ),
+			// Resolves theme (imported by base-styles' mixins)
+			path.join( PACKAGES_DIR, 'wp-build', 'node_modules' ),
 			// For local imports like @use "mixins"
 			path.join( PACKAGES_DIR, 'base-styles' ),
 		],

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.

Hmm, makes me think we should have some test/safeguard to catch those.

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.

No, we need #76195, which needs #72143 or #72973 to happen first.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Thanks for the heads up @manzoorwanijk 🙏 I'm going to try removing the dep from base-styles, it's causing a lot of hassle that I specifically am trying to avoid introducing here.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Addressed in #79651

@use "./variables";
@use "./colors";
@use "./breakpoints";
Expand Down Expand Up @@ -180,6 +181,19 @@
}


/**
* Standard focus rings for the WordPress Design System.
*
* Apply `outset-ring__focus` inside the relevant pseudo-class at the call site,
* e.g. `&:focus { @include outset-ring__focus(); }`.
*/

Comment thread
mirka marked this conversation as resolved.
Resolved
@mixin outset-ring__focus() {
outline: wpds.var("--wpds-border-width-focus") solid wpds.var("--wpds-color-stroke-focus");
outline-offset: wpds.var("--wpds-border-width-focus");
}


/**
* Applies editor left position to the selector passed as argument
*/
Expand Down
3 changes: 3 additions & 0 deletions packages/base-styles/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@
"./z-index": "./_z-index.scss"
},
"wpScript": true,
"dependencies": {
"@wordpress/theme": "file:../theme"
},
"publishConfig": {
"access": "public"
}
Expand Down
4 changes: 4 additions & 0 deletions packages/theme/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## Unreleased

### New Features

- Export `@wordpress/theme/utils` with a generated `var()` Sass function for compile-time design token fallback injection ([#79470](https://github.com/WordPress/gutenberg/pull/79470)).

### Bug Fixes

- Mark the published `design-tokens.css` file as side-effectful so downstream bundlers preserve the documented CSS import ([#79551](https://github.com/WordPress/gutenberg/pull/79551)).
Expand Down
34 changes: 31 additions & 3 deletions packages/theme/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,11 @@ If your application renders React content into additional documents (an iframe,

### Developer Tools

For the best development experience, we recommend configuring the [build plugins](#build-plugins) and [Stylelint rules](#stylelint-plugins) provided by this package. The build plugins automatically inject fallback values into `var(--wpds-*)` references so components render correctly even when the tokens stylesheet is not yet loaded, and will raise an error if a reference does not match a known token. The Stylelint rules catch typos, unknown tokens, and other discouraged patterns during development.
For the best development experience, we recommend configuring the [Stylelint rules](#stylelint-plugins) provided by this package. The Stylelint rules catch typos, unknown tokens, and other discouraged patterns during development.

If you use `@wordpress/build` to build your scripts, the build plugins are already enabled by default.
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.

If you write Sass/SCSS but cannot load the design tokens stylesheet, add build plugins to your pipeline, or use `@wordpress/build`, use the [Sass helper](#sass-helper) instead.

## Theme Provider

Expand Down Expand Up @@ -228,7 +230,9 @@ This rule reports an error when a CSS declaration sets (defines) a custom proper

### `plugin-wpds/no-token-fallback-values`

This rule reports an error when a `var()` call for a `--wpds-*` token includes a manual fallback value. Fallback values for design tokens are injected automatically at build time by the [build plugins](#build-plugins), so manual fallbacks in source are redundant and can drift out of sync with the token definitions.
This rule reports an error when a `var()` call for a `--wpds-*` token includes a manual fallback value. In CSS processed by the [build plugins](#build-plugins), fallback values are injected automatically, so manual fallbacks in `var(--wpds-*)` references are redundant and can drift out of sync with the token definitions.

For Sass sources that cannot use the build plugins, use the [Sass helper](#sass-helper) instead of writing manual fallback values.

```css
/* ✗ Error: Do not add a fallback value for Design System token '--wpds-color-foreground-content-neutral' */
Expand Down Expand Up @@ -304,6 +308,30 @@ export default defineConfig( {
} );
```

## Sass helper

An alternative to loading the [design tokens stylesheet](#outside-wordpress), using the [build plugins](#build-plugins), or `@wordpress/build`. Use it when you write Sass/SCSS and cannot change your build setup to support any of those options.

You do not need this helper if you already load the design tokens stylesheet or use fallback injection via the build plugins or `@wordpress/build`. In those cases, write bare `var(--wpds-*)` references in your source.

The `@wordpress/theme/utils` export provides a generated `var()` function that wraps token references with the same fallback map used by the build plugins. For example, `wpds.var('--wpds-color-foreground-content-neutral')` compiles to `var(--wpds-color-foreground-content-neutral, #1e1e1e)`.

```scss
@use '@wordpress/theme/utils' as wpds;

.example {
color: wpds.var( '--wpds-color-foreground-content-neutral' );
}
```

When using Sass with [`NodePackageImporter`](https://sass-lang.com/documentation/js-api/classes/nodepackageimporter/), you can import via the `pkg:` scheme:

```scss
@use 'pkg:@wordpress/theme/utils' as wpds;
```

The same `@wordpress/theme/utils` import also supports Sass setups that resolve packages from `node_modules` with a load path.

## Contributing to this package

This is an individual package that's part of the Gutenberg project. The project is organized as a monorepo. It's made up of multiple self-contained software packages, each with a specific purpose. The packages in this monorepo are published to [npm](https://www.npmjs.com/) and used by [WordPress](https://make.wordpress.org/core/) as well as other software projects.
Expand Down
67 changes: 58 additions & 9 deletions packages/theme/bin/terrazzo-plugin-ds-token-fallbacks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,8 +186,50 @@ export function computeBrandFallback( stepHex: string ): string {
}%, ${ mixWith })`;
}

function escapeScssString( value: string ): string {
return value.replace( /\\/g, '\\\\' ).replace( /'/g, "\\'" );
}

export function formatDesignTokenFallbacksScss(
fallbacks: Record< string, string >
): string {
const mapEntries = Object.entries( fallbacks )
.map( ( [ token, fallback ] ) => {
if ( fallback.includes( '#{' ) ) {
throw new Error(
`Sass interpolation syntax is not supported in design token fallbacks: ${ token }`
);
}

return `\t'${ token }': '${ escapeScssString( fallback ) }'`;
} )
.join( ',\n' );

return [
'// This file is generated by the @wordpress/terrazzo-plugin-ds-token-fallbacks plugin.',
'// Do not edit this file directly.',
'',
'@use "sass:map";',
'@use "sass:string";',
'',
'$fallbacks: (',
mapEntries,
');',
'',
'@function var($token) {',
'\t@if not map.has-key($fallbacks, $token) {',
'\t\t@error "Unknown design token: #{$token}";',
'\t}',
'',
'\t@return string.unquote("var(#{$token}, #{map.get($fallbacks, $token)})");',
'}',
'',
].join( '\n' );
}

export default function pluginDsTokenFallbacks( {
filename = 'js/design-token-fallbacks.mjs',
scssFilename = 'scss/design-token-fallbacks.scss',
} = {} ): Plugin {
return {
name: '@wordpress/terrazzo-plugin-ds-token-fallbacks',
Expand Down Expand Up @@ -280,17 +322,24 @@ export default function pluginDsTokenFallbacks( {
)
);

const generatedHeader = [
'/*',
' * This file is generated by the @wordpress/terrazzo-plugin-ds-token-fallbacks plugin.',
' * Do not edit this file directly.',
' */',
'',
].join( '\n' );

outputFile(
filename,
[
'/*',
' * This file is generated by the @wordpress/terrazzo-plugin-ds-token-fallbacks plugin.',
' * Do not edit this file directly.',
' */',
'',
`export default ${ JSON.stringify( sorted, null, '\t' ) }`,
'',
].join( '\n' )
generatedHeader +
`export default ${ JSON.stringify( sorted, null, '\t' ) }` +
'\n'
);

outputFile(
scssFilename,
formatDesignTokenFallbacksScss( sorted )
);
},
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { computeBrandFallback } from '../index';
import { computeBrandFallback, formatDesignTokenFallbacksScss } from '../index';

jest.mock( '@terrazzo/plugin-css', () => ( { FORMAT_ID: 'css/value' } ) );

Expand All @@ -15,3 +15,47 @@ describe( 'computeBrandFallback', () => {
);
} );
} );

describe( 'formatDesignTokenFallbacksScss', () => {
it( 'generates a Sass map and var function', () => {
const scss = formatDesignTokenFallbacksScss( {
'--wpds-border-radius-sm': '2px',
'--wpds-border-width-focus':
'var(--wp-admin-border-width-focus, 2px)',
'--wpds-typography-font-family-mono':
'"Menlo", "Consolas", monaco, monospace',
} );

expect( scss ).toContain( '@use "sass:map";' );
expect( scss ).toContain( '@function var($token)' );
expect( scss ).toContain( "'--wpds-border-radius-sm': '2px'" );
expect( scss ).toContain(
"'--wpds-border-width-focus': 'var(--wp-admin-border-width-focus, 2px)'"
);
expect( scss ).toContain(
'\'--wpds-typography-font-family-mono\': \'"Menlo", "Consolas", monaco, monospace\''
);
} );

it( 'escapes backslashes and single quotes', () => {
const scss = formatDesignTokenFallbacksScss( {
'--wpds-x-single-quote': "a'b",
'--wpds-x-backslash': 'a\\b',
'--wpds-x-both': "a'\\b",
} );

expect( scss ).toContain( "'--wpds-x-single-quote': 'a\\'b'" );
expect( scss ).toContain( "'--wpds-x-backslash': 'a\\\\b'" );
expect( scss ).toContain( "'--wpds-x-both': 'a\\'\\\\b'" );
} );

it( 'throws on Sass interpolation syntax', () => {
expect( () =>
formatDesignTokenFallbacksScss( {
'--wpds-x-interpolation': 'calc(#{some-value} * 1px)',
} )
).toThrow(
/Sass interpolation syntax is not supported in design token fallbacks: --wpds-x-interpolation/
);
} );
} );
4 changes: 4 additions & 0 deletions packages/theme/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"build",
"build-module",
"build-types",
"utils.scss",
"*.md"
],
"main": "build/index.cjs",
Expand All @@ -38,6 +39,9 @@
"default": "./build/index.cjs"
},
"./design-tokens.css": "./src/prebuilt/css/design-tokens.css",
"./utils": {
"sass": "./utils.scss"
},
"./design-tokens.js": {
"types": "./build-types/prebuilt/js/design-tokens.d.ts",
"import": "./build-module/prebuilt/js/design-tokens.mjs",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
* This file is generated by the @wordpress/terrazzo-plugin-ds-token-fallbacks plugin.
* Do not edit this file directly.
*/

export default {
'--wpds-border-radius-lg': '8px',
'--wpds-border-radius-md': '4px',
Expand Down
Loading
Loading