From b533504e0a081626b38bcb2da7b10b779c70c30b Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Tue, 27 Jan 2026 15:07:48 -0500 Subject: [PATCH 1/5] UI: Remove Box component abstraction --- packages/ui/CHANGELOG.md | 4 + packages/ui/README.md | 25 ++--- packages/ui/src/badge/badge.tsx | 74 +++++++------- packages/ui/src/box/box.tsx | 107 -------------------- packages/ui/src/box/index.ts | 1 - packages/ui/src/box/stories/index.story.tsx | 49 --------- packages/ui/src/box/test/box.test.tsx | 29 ------ packages/ui/src/box/types.ts | 61 ----------- packages/ui/src/index.ts | 1 - 9 files changed, 53 insertions(+), 298 deletions(-) delete mode 100644 packages/ui/src/box/box.tsx delete mode 100644 packages/ui/src/box/index.ts delete mode 100644 packages/ui/src/box/stories/index.story.tsx delete mode 100644 packages/ui/src/box/test/box.test.tsx delete mode 100644 packages/ui/src/box/types.ts diff --git a/packages/ui/CHANGELOG.md b/packages/ui/CHANGELOG.md index 91370e60ceebe5..c949cc9bcc3d5b 100644 --- a/packages/ui/CHANGELOG.md +++ b/packages/ui/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Breaking Changes + +- Remove `Box` component. Components that previously used `Box` should use the equivalent design tokens in their CSS directly ([#74981](https://github.com/WordPress/gutenberg/issues/74981)). + ### New Features - Add `Tabs` primitive ([#74652](https://github.com/WordPress/gutenberg/pull/74652)). diff --git a/packages/ui/README.md b/packages/ui/README.md index ef1a3ad5a59537..f22f7a3a076c02 100644 --- a/packages/ui/README.md +++ b/packages/ui/README.md @@ -39,13 +39,14 @@ import '@wordpress/theme/design-tokens.css'; ### Basic Component Usage ```tsx -import { Box } from '@wordpress/ui'; +import { Stack } from '@wordpress/ui'; function MyComponent() { return ( - - Hello World - + +
Item 1
+
Item 2
+
); } ``` @@ -59,11 +60,11 @@ All components in the design system follow consistent patterns for maximum flexi Every component supports the `render` prop for complete control over the underlying HTML element: ```tsx -import { Box } from '@wordpress/ui'; +import { Stack } from '@wordpress/ui'; function MyComponent() { - // Render Box as a instead of the default
- return }>{ /* ... */ }; + // Render Stack as a
instead of the default
+ return }>{ /* ... */ }; } ``` @@ -73,12 +74,12 @@ All components forward refs to their underlying DOM elements: ```tsx import { useRef } from '@wordpress/element'; -import { Box } from '@wordpress/ui'; +import { Stack } from '@wordpress/ui'; function MyComponent() { - const boxRef = useRef< HTMLDivElement >( null ); + const stackRef = useRef< HTMLDivElement >( null ); - return { /* ... */ }; + return { /* ... */ }; } ``` @@ -87,11 +88,11 @@ function MyComponent() { Components merge provided `className` props with their internal styles: ```tsx -import { Box } from '@wordpress/ui'; +import { Stack } from '@wordpress/ui'; function MyComponent() { // Your custom CSS className is merged with component styles - return { /* ... */ }; + return { /* ... */ }; } ``` diff --git a/packages/ui/src/badge/badge.tsx b/packages/ui/src/badge/badge.tsx index 054f46066a9220..32e6dde53cca54 100644 --- a/packages/ui/src/badge/badge.tsx +++ b/packages/ui/src/badge/badge.tsx @@ -1,6 +1,5 @@ +import { useRender, mergeProps } from '@base-ui/react'; import { forwardRef } from '@wordpress/element'; -import { Box } from '../box'; -import { type BoxProps } from '../box/types'; import { type BadgeProps } from './types'; /** @@ -11,55 +10,53 @@ const DEFAULT_RENDER = ( props: React.ComponentPropsWithoutRef< 'span' > ) => ( ); /** - * Maps intent values to Box backgroundColor and color props. - * Uses strong emphasis styles (as emphasis prop has been removed). + * Maps intent values to CSS styles using design tokens. */ const getIntentStyles = ( intent: BadgeProps[ 'intent' ] -): Partial< BoxProps > => { +): React.CSSProperties => { switch ( intent ) { case 'high': return { - backgroundColor: 'error', - color: 'error', + backgroundColor: 'var(--wpds-color-bg-surface-error)', + color: 'var(--wpds-color-fg-content-error)', }; case 'medium': return { - backgroundColor: 'warning', - color: 'warning', + backgroundColor: 'var(--wpds-color-bg-surface-warning)', + color: 'var(--wpds-color-fg-content-warning)', }; case 'low': return { - backgroundColor: 'caution', - color: 'caution', + backgroundColor: 'var(--wpds-color-bg-surface-caution)', + color: 'var(--wpds-color-fg-content-caution)', }; case 'stable': return { - backgroundColor: 'success', - color: 'success', + backgroundColor: 'var(--wpds-color-bg-surface-success)', + color: 'var(--wpds-color-fg-content-success)', }; case 'informational': return { - backgroundColor: 'info', - color: 'info', + backgroundColor: 'var(--wpds-color-bg-surface-info)', + color: 'var(--wpds-color-fg-content-info)', }; case 'draft': return { - backgroundColor: 'neutral-weak', - color: 'neutral', + backgroundColor: 'var(--wpds-color-bg-surface-neutral-weak)', + color: 'var(--wpds-color-fg-content-neutral)', }; case 'none': default: return { - backgroundColor: 'neutral', - color: 'neutral-weak', + backgroundColor: 'var(--wpds-color-bg-surface-neutral)', + color: 'var(--wpds-color-fg-content-neutral-weak)', }; } }; /** * A badge component for displaying labels with semantic intent. - * Built on the Box primitive for consistent theming and accessibility. */ export const Badge = forwardRef< HTMLDivElement, BadgeProps >( function Badge( { children, intent = 'none', render = DEFAULT_RENDER, ...props }, @@ -67,22 +64,23 @@ export const Badge = forwardRef< HTMLDivElement, BadgeProps >( function Badge( ) { const intentStyles = getIntentStyles( intent ); - return ( - - { children } - - ); + const style: React.CSSProperties = { + ...intentStyles, + paddingInline: 'var(--wpds-dimension-padding-sm)', + paddingBlock: 'var(--wpds-dimension-padding-xs)', + borderRadius: 'var(--wpds-border-radius-lg)', + fontFamily: 'var(--wpds-font-family-body)', + fontSize: 'var(--wpds-font-size-sm)', + fontWeight: 'var(--wpds-font-weight-regular)', + lineHeight: 'var(--wpds-font-line-height-xs)', + ...props.style, + }; + + const element = useRender( { + render, + ref, + props: mergeProps< 'span' >( props, { style, children } ), + } ); + + return element; } ); diff --git a/packages/ui/src/box/box.tsx b/packages/ui/src/box/box.tsx deleted file mode 100644 index 180fb387645893..00000000000000 --- a/packages/ui/src/box/box.tsx +++ /dev/null @@ -1,107 +0,0 @@ -import { useRender, mergeProps } from '@base-ui/react'; -import { forwardRef } from '@wordpress/element'; -import { type BoxProps } from './types'; - -/** - * Default render function that renders a div element with the given props. - */ -const DEFAULT_RENDER = ( props: React.ComponentPropsWithoutRef< 'div' > ) => ( -
-); - -/** - * Capitalizes the first character of a string. - */ -const capitalize = ( str: string ): string => - str.charAt( 0 ).toUpperCase() + str.slice( 1 ); - -/** - * Converts a size token name to a CSS design token property reference (with - * fallback). - * - * @param property The CSS property name. - * @param value The size token name. - * @return A CSS value string with variable references. - */ -const getSpacingValue = ( property: string, value: string ): string => - `var(--wpds-dimension-${ property }-${ value }, var(--wpds-dimension-${ property }-${ value }))`; - -/** - * Generates CSS styles for properties with optionally directional values, - * normalizing single values and objects with directional keys for logical - * properties. - * - * @param property The CSS property name from BoxProps. - * @param value The property value (single or object with directional keys). - * @return A CSSProperties object with the computed styles. - */ -const getDimensionVariantStyles = < T extends keyof BoxProps >( - property: T, - value: NonNullable< BoxProps[ T ] > -): React.CSSProperties => - typeof value !== 'object' - ? { [ property ]: getSpacingValue( property, value ) } - : Object.keys( value ).reduce( - ( result, key ) => ( { - ...result, - [ property + capitalize( key ) ]: getSpacingValue( - property, - value[ key ] - ), - } ), - {} as Record< string, string > - ); - -/** - * A low-level visual primitive that provides an interface for applying design - * token-based customization for background, text, padding, and more. - */ -export const Box = forwardRef< HTMLDivElement, BoxProps >( function Box( - { - target = 'surface', - backgroundColor, - color, - padding, - borderRadius, - borderWidth, - borderColor, - render = DEFAULT_RENDER, - ...props - }, - ref -) { - const style: React.CSSProperties = {}; - - if ( backgroundColor ) { - style.backgroundColor = `var(--wpds-color-bg-${ target }-${ backgroundColor }, var(--wpds-color-bg-surface-${ backgroundColor }))`; - } - - if ( color ) { - style.color = `var(--wpds-color-fg-${ target }-${ color }, var(--wpds-color-fg-content-${ color }))`; - } - - if ( padding ) { - Object.assign( style, getDimensionVariantStyles( 'padding', padding ) ); - } - - if ( borderRadius ) { - style.borderRadius = `var(--wpds-border-radius-${ target }-${ borderRadius }, var(--wpds-border-radius-${ borderRadius }))`; - } - - if ( borderWidth ) { - style.borderWidth = `var(--wpds-border-width-${ target }-${ borderWidth }, var(--wpds-border-width-${ borderWidth }))`; - style.borderStyle = 'solid'; - } - - if ( borderColor ) { - style.borderColor = `var(--wpds-color-stroke-${ target }-${ borderColor }, var(--wpds-color-stroke-surface-${ borderColor }))`; - } - - const element = useRender( { - render, - ref, - props: mergeProps< 'div' >( props, { style } ), - } ); - - return element; -} ); diff --git a/packages/ui/src/box/index.ts b/packages/ui/src/box/index.ts deleted file mode 100644 index aae32603581ca3..00000000000000 --- a/packages/ui/src/box/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { Box } from './box'; diff --git a/packages/ui/src/box/stories/index.story.tsx b/packages/ui/src/box/stories/index.story.tsx deleted file mode 100644 index 0f5614432d8a44..00000000000000 --- a/packages/ui/src/box/stories/index.story.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import { type Meta, type StoryObj } from '@storybook/react-vite'; -import { type PaddingSize } from '@wordpress/theme'; -import { Box } from '../box'; - -const meta: Meta< typeof Box > = { - title: 'Design System/Components/Box', - component: Box, -}; -export default meta; - -type Story = StoryObj< typeof Box >; - -export const Default: Story = { - args: { - children: 'Box', - backgroundColor: 'info', - color: 'info', - padding: 'sm', - borderColor: 'brand', - borderRadius: 'md', - borderWidth: 'sm', - }, - argTypes: { - padding: { - control: 'select', - options: [ - 'xs', - 'sm', - 'md', - 'lg', - 'xl', - '2xl', - '3xl', - ] satisfies PaddingSize[], - }, - }, -}; - -export const DirectionalPadding: Story = { - ...Default, - args: { - ...Default.args, - padding: { - blockStart: 'sm', - inline: 'md', - blockEnd: 'lg', - }, - }, -}; diff --git a/packages/ui/src/box/test/box.test.tsx b/packages/ui/src/box/test/box.test.tsx deleted file mode 100644 index 912dbafdf49005..00000000000000 --- a/packages/ui/src/box/test/box.test.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { render, screen } from '@testing-library/react'; -import { createRef } from '@wordpress/element'; -import { Box } from '../box'; - -describe( 'Box', () => { - it( 'forwards ref', () => { - const ref = createRef< HTMLDivElement >(); - - render( Content ); - - expect( ref.current ).toBeInstanceOf( HTMLDivElement ); - } ); - - it( 'merges props', () => { - render( - - Content - - ); - - const box = screen.getByText( 'Content' ); - - expect( box ).toHaveStyle( { - 'background-color': - 'var(--wpds-color-bg-surface-brand, var(--wpds-color-bg-surface-brand))', - width: '10px', - } ); - } ); -} ); diff --git a/packages/ui/src/box/types.ts b/packages/ui/src/box/types.ts deleted file mode 100644 index 22bae4eb9e1094..00000000000000 --- a/packages/ui/src/box/types.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { - type PaddingSize, - type BorderRadiusSize, - type BorderWidthSize, - type Target, - type SurfaceBackgroundColor, - type ContentForegroundColor, - type SurfaceStrokeColor, -} from '@wordpress/theme'; -import { type ComponentProps } from '../utils/types'; - -type DimensionVariant< T > = { - block?: T; - blockStart?: T; - blockEnd?: T; - inline?: T; - inlineStart?: T; - inlineEnd?: T; -}; - -export interface BoxProps extends ComponentProps< 'div' > { - /** - * The target rendering element design token grouping to use for the box. - */ - target?: Target; - - /** - * The surface background design token for box background color. - */ - backgroundColor?: SurfaceBackgroundColor; - - /** - * The surface foreground design token for box text color. - */ - color?: ContentForegroundColor; - - /** - * The surface spacing design token or base unit multiplier for box padding. - */ - padding?: PaddingSize | DimensionVariant< PaddingSize >; - - /** - * The surface border radius design token. - */ - borderRadius?: BorderRadiusSize; - - /** - * The surface border width design token. - */ - borderWidth?: BorderWidthSize; - - /** - * The surface border stroke color design token. - */ - borderColor?: SurfaceStrokeColor; - - /** - * The content to be rendered inside the component. - */ - children?: React.ReactNode; -} diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts index 64c8c05f34ec93..b303bd87834b9d 100644 --- a/packages/ui/src/index.ts +++ b/packages/ui/src/index.ts @@ -1,5 +1,4 @@ export * from './badge'; -export * from './box'; export * from './button'; export * from './form/primitives'; export * from './icon'; From 68e7713160f5d97fa490378d7a52ade836dc48d4 Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Tue, 27 Jan 2026 15:13:12 -0500 Subject: [PATCH 2/5] Update Stack stories to remove references to Box --- packages/ui/src/stack/stories/index.story.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/ui/src/stack/stories/index.story.tsx b/packages/ui/src/stack/stories/index.story.tsx index 66ab560c04c314..4dd2019beb93d7 100644 --- a/packages/ui/src/stack/stories/index.story.tsx +++ b/packages/ui/src/stack/stories/index.story.tsx @@ -1,6 +1,5 @@ import type { Meta, StoryObj } from '@storybook/react-vite'; import { Stack } from '../index'; -import { Box } from '../../box'; const meta: Meta< typeof Stack > = { title: 'Design System/Components/Stack', @@ -9,9 +8,9 @@ const meta: Meta< typeof Stack > = { export default meta; const DemoBox = ( { variant }: { variant?: 'lg' } ) => ( - Date: Tue, 27 Jan 2026 15:18:29 -0500 Subject: [PATCH 3/5] Align Badge forwarded ref generic to rendered element --- packages/ui/src/badge/badge.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ui/src/badge/badge.tsx b/packages/ui/src/badge/badge.tsx index 32e6dde53cca54..061f32e200e54c 100644 --- a/packages/ui/src/badge/badge.tsx +++ b/packages/ui/src/badge/badge.tsx @@ -58,7 +58,7 @@ const getIntentStyles = ( /** * A badge component for displaying labels with semantic intent. */ -export const Badge = forwardRef< HTMLDivElement, BadgeProps >( function Badge( +export const Badge = forwardRef< HTMLSpanElement, BadgeProps >( function Badge( { children, intent = 'none', render = DEFAULT_RENDER, ...props }, ref ) { From f4ee7f6da484762b8b2021ee6a878dcda7bddd2c Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Tue, 27 Jan 2026 15:19:32 -0500 Subject: [PATCH 4/5] Use defaultTagName over function for default render Simpler, likely more performant --- packages/ui/src/badge/badge.tsx | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/packages/ui/src/badge/badge.tsx b/packages/ui/src/badge/badge.tsx index 061f32e200e54c..0bacbaa9a79744 100644 --- a/packages/ui/src/badge/badge.tsx +++ b/packages/ui/src/badge/badge.tsx @@ -2,13 +2,6 @@ import { useRender, mergeProps } from '@base-ui/react'; import { forwardRef } from '@wordpress/element'; import { type BadgeProps } from './types'; -/** - * Default render function that renders a span element with the given props. - */ -const DEFAULT_RENDER = ( props: React.ComponentPropsWithoutRef< 'span' > ) => ( - -); - /** * Maps intent values to CSS styles using design tokens. */ @@ -59,7 +52,7 @@ const getIntentStyles = ( * A badge component for displaying labels with semantic intent. */ export const Badge = forwardRef< HTMLSpanElement, BadgeProps >( function Badge( - { children, intent = 'none', render = DEFAULT_RENDER, ...props }, + { children, intent = 'none', render, ...props }, ref ) { const intentStyles = getIntentStyles( intent ); @@ -78,6 +71,7 @@ export const Badge = forwardRef< HTMLSpanElement, BadgeProps >( function Badge( const element = useRender( { render, + defaultTagName: 'span', ref, props: mergeProps< 'span' >( props, { style, children } ), } ); From 3e4cfef6be52621bf6880ad7d33da4e9905869c5 Mon Sep 17 00:00:00 2001 From: Andrew Duthie Date: Thu, 5 Feb 2026 14:42:24 -0500 Subject: [PATCH 5/5] Shift Badge styles to stylesheet --- packages/ui/src/badge/badge.tsx | 73 ++++---------------------- packages/ui/src/badge/style.module.css | 48 +++++++++++++++++ 2 files changed, 59 insertions(+), 62 deletions(-) create mode 100644 packages/ui/src/badge/style.module.css diff --git a/packages/ui/src/badge/badge.tsx b/packages/ui/src/badge/badge.tsx index 0bacbaa9a79744..abd9fc40bf4616 100644 --- a/packages/ui/src/badge/badge.tsx +++ b/packages/ui/src/badge/badge.tsx @@ -1,79 +1,28 @@ import { useRender, mergeProps } from '@base-ui/react'; +import clsx from 'clsx'; import { forwardRef } from '@wordpress/element'; import { type BadgeProps } from './types'; - -/** - * Maps intent values to CSS styles using design tokens. - */ -const getIntentStyles = ( - intent: BadgeProps[ 'intent' ] -): React.CSSProperties => { - switch ( intent ) { - case 'high': - return { - backgroundColor: 'var(--wpds-color-bg-surface-error)', - color: 'var(--wpds-color-fg-content-error)', - }; - case 'medium': - return { - backgroundColor: 'var(--wpds-color-bg-surface-warning)', - color: 'var(--wpds-color-fg-content-warning)', - }; - case 'low': - return { - backgroundColor: 'var(--wpds-color-bg-surface-caution)', - color: 'var(--wpds-color-fg-content-caution)', - }; - case 'stable': - return { - backgroundColor: 'var(--wpds-color-bg-surface-success)', - color: 'var(--wpds-color-fg-content-success)', - }; - case 'informational': - return { - backgroundColor: 'var(--wpds-color-bg-surface-info)', - color: 'var(--wpds-color-fg-content-info)', - }; - case 'draft': - return { - backgroundColor: 'var(--wpds-color-bg-surface-neutral-weak)', - color: 'var(--wpds-color-fg-content-neutral)', - }; - case 'none': - default: - return { - backgroundColor: 'var(--wpds-color-bg-surface-neutral)', - color: 'var(--wpds-color-fg-content-neutral-weak)', - }; - } -}; +import styles from './style.module.css'; /** * A badge component for displaying labels with semantic intent. */ export const Badge = forwardRef< HTMLSpanElement, BadgeProps >( function Badge( - { children, intent = 'none', render, ...props }, + { children, intent = 'none', render, className, ...props }, ref ) { - const intentStyles = getIntentStyles( intent ); - - const style: React.CSSProperties = { - ...intentStyles, - paddingInline: 'var(--wpds-dimension-padding-sm)', - paddingBlock: 'var(--wpds-dimension-padding-xs)', - borderRadius: 'var(--wpds-border-radius-lg)', - fontFamily: 'var(--wpds-font-family-body)', - fontSize: 'var(--wpds-font-size-sm)', - fontWeight: 'var(--wpds-font-weight-regular)', - lineHeight: 'var(--wpds-font-line-height-xs)', - ...props.style, - }; - const element = useRender( { render, defaultTagName: 'span', ref, - props: mergeProps< 'span' >( props, { style, children } ), + props: mergeProps< 'span' >( props, { + className: clsx( + styles.badge, + styles[ `is-${ intent }-intent` ], + className + ), + children, + } ), } ); return element; diff --git a/packages/ui/src/badge/style.module.css b/packages/ui/src/badge/style.module.css new file mode 100644 index 00000000000000..fe339a844c78a6 --- /dev/null +++ b/packages/ui/src/badge/style.module.css @@ -0,0 +1,48 @@ +@layer wp-ui-utilities, wp-ui-components, wp-ui-compositions, wp-ui-overrides; + +@layer wp-ui-components { + .badge { + padding-inline: var(--wpds-dimension-padding-sm); + padding-block: var(--wpds-dimension-padding-xs); + border-radius: var(--wpds-border-radius-lg); + font-family: var(--wpds-font-family-body); + font-size: var(--wpds-font-size-sm); + font-weight: var(--wpds-font-weight-regular); + line-height: var(--wpds-font-line-height-xs); + } + + .is-high-intent { + background-color: var(--wpds-color-bg-surface-error); + color: var(--wpds-color-fg-content-error); + } + + .is-medium-intent { + background-color: var(--wpds-color-bg-surface-warning); + color: var(--wpds-color-fg-content-warning); + } + + .is-low-intent { + background-color: var(--wpds-color-bg-surface-caution); + color: var(--wpds-color-fg-content-caution); + } + + .is-stable-intent { + background-color: var(--wpds-color-bg-surface-success); + color: var(--wpds-color-fg-content-success); + } + + .is-informational-intent { + background-color: var(--wpds-color-bg-surface-info); + color: var(--wpds-color-fg-content-info); + } + + .is-draft-intent { + background-color: var(--wpds-color-bg-surface-neutral-weak); + color: var(--wpds-color-fg-content-neutral); + } + + .is-none-intent { + background-color: var(--wpds-color-bg-surface-neutral); + color: var(--wpds-color-fg-content-neutral-weak); + } +}