Skip to content
Open
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
ced7ad2
export all utilities
reidbarber Jan 28, 2026
418056f
export remaining utils
reidbarber Jan 28, 2026
2ccfd61
Merge remote-tracking branch 'origin/main' into style-macr-utility-audit
reidbarber Feb 11, 2026
fe2bc92
removed unsafe exports for now
reidbarber Feb 11, 2026
4e620c3
add JSDoc descriptions
reidbarber Feb 11, 2026
0cf5ad7
rename colorScheme() to setColorScheme()
reidbarber Feb 11, 2026
acf0f38
remove raw/keyframes from s2 export
reidbarber Feb 11, 2026
9f6eb4e
export WidthProperties and HeightProperties as types
reidbarber Feb 11, 2026
2034cc0
cleanup JSDocs
reidbarber Feb 11, 2026
68a080f
add docs
reidbarber Feb 11, 2026
ef9ab31
lint
reidbarber Feb 11, 2026
afcd187
Merge remote-tracking branch 'origin/main' into style-macr-utility-audit
reidbarber Feb 23, 2026
c8be472
address review comments
reidbarber Feb 24, 2026
efcd314
extract docs from JSDoc
reidbarber Feb 24, 2026
3003c94
add imports to all examples
reidbarber Feb 24, 2026
c6e6cde
update styles->className in example
reidbarber Mar 9, 2026
15afa36
fix size in md output
reidbarber Mar 9, 2026
302c704
add linearGradient
reidbarber Mar 9, 2026
8ef8ff0
Merge remote-tracking branch 'origin/main' into style-macr-utility-audit
reidbarber Mar 9, 2026
8cc7ebf
remove getAllowedOverrides from exports/docs (could have breaking cha…
reidbarber Mar 10, 2026
4113557
Merge remote-tracking branch 'origin/main' into style-macr-utility-audit
reidbarber Mar 10, 2026
e5c2fd3
remove WidthProperties/HeightProperties since we removed getAllowedOv…
reidbarber Mar 13, 2026
1d716d5
Merge remote-tracking branch 'origin/main' into style-macr-utility-audit
reidbarber Mar 13, 2026
74b6f70
address review comments
reidbarber Mar 17, 2026
12a29ff
Merge remote-tracking branch 'origin/main' into style-macr-utility-audit
reidbarber Mar 17, 2026
d6b5850
rename raw -> css
reidbarber Mar 17, 2026
e2fac29
Merge remote-tracking branch 'origin/main' into style-macr-utility-audit
reidbarber Mar 17, 2026
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
4 changes: 2 additions & 2 deletions packages/@react-spectrum/s2/src/CoachMark.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ import {
import {ButtonContext} from './Button';
import {Card} from './Card';
import {CheckboxContext} from './Checkbox';
import {colorScheme, getAllowedOverrides, StyleProps} from './style-utils' with {type: 'macro'};
import {ColorSchemeContext} from './Provider';
import {ContentContext, FooterContext, KeyboardContext, TextContext} from './Content';
import {
Expand All @@ -40,6 +39,7 @@ import {
} from 'react';
import {DividerContext} from './Divider';
import {forwardRefType} from './types';
import {getAllowedOverrides, setColorScheme, StyleProps} from './style-utils' with {type: 'macro'};
import {GlobalDOMAttributes} from '@react-types/shared';
import {ImageContext} from './Image';
import {ImageCoordinator} from './ImageCoordinator';
Expand Down Expand Up @@ -106,7 +106,7 @@ const slideLeftKeyframes = keyframes(`
`);

let popover = style({
...colorScheme(),
...setColorScheme(),
'--s2-container-bg': {
type: 'backgroundColor',
value: 'layer-2'
Expand Down
4 changes: 2 additions & 2 deletions packages/@react-spectrum/s2/src/Modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@
* governing permissions and limitations under the License.
*/

import {colorScheme} from './style-utils' with {type: 'macro'};
import {ColorSchemeContext} from './Provider';
import {DOMRef, GlobalDOMAttributes} from '@react-types/shared';
import {forwardRef, MutableRefObject, useCallback, useContext} from 'react';
import {ModalOverlay, ModalOverlayProps, Modal as RACModal, useLocale} from 'react-aria-components';
import {setColorScheme} from './style-utils' with {type: 'macro'};
import {style} from '../style' with {type: 'macro'};
import {useDOMRef} from '@react-spectrum/utils';

Expand All @@ -28,7 +28,7 @@ interface ModalProps extends Omit<ModalOverlayProps, 'className' | 'style' | 're
}

const modalOverlayStyles = style({
...colorScheme(),
...setColorScheme(),
position: 'absolute',
top: 0,
left: 0,
Expand Down
4 changes: 2 additions & 2 deletions packages/@react-spectrum/s2/src/Popover.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@ import {
OverlayTriggerStateContext,
useLocale
} from 'react-aria-components';
import {colorScheme, getAllowedOverrides, heightProperties, UnsafeStyles, widthProperties} from './style-utils' with {type: 'macro'};
import {ColorSchemeContext} from './Provider';
import {createContext, ForwardedRef, forwardRef, ReactNode, useCallback, useContext, useMemo} from 'react';
import {DOMRef, DOMRefValue, GlobalDOMAttributes} from '@react-types/shared';
import {getAllowedOverrides, heightProperties, setColorScheme, UnsafeStyles, widthProperties} from './style-utils' with {type: 'macro'};
import {lightDark, style} from '../style' with {type: 'macro'};
import {mergeRefs} from '@react-aria/utils';
import {mergeStyles} from '../style/runtime';
Expand Down Expand Up @@ -62,7 +62,7 @@ export interface PopoverProps extends UnsafeStyles, Omit<AriaPopoverProps,
}

let popover = style({
...colorScheme(),
...setColorScheme(),
'--s2-container-bg': {
type: 'backgroundColor',
value: {
Expand Down
4 changes: 2 additions & 2 deletions packages/@react-spectrum/s2/src/Provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,14 @@
*/

import type {ColorScheme, Router} from '@react-types/provider';
import {colorScheme, UnsafeStyles} from './style-utils' with {type: 'macro'};
import {createContext, JSX, ReactNode, useContext} from 'react';
import {DOMProps} from '@react-types/shared';
import {filterDOMProps} from '@react-aria/utils';
import {Fonts} from './Fonts';
import {generateDefaultColorSchemeStyles} from './page.macro' with {type: 'macro'};
import {I18nProvider, RouterProvider, useLocale} from 'react-aria-components';
import {mergeStyles} from '../style/runtime';
import {setColorScheme, UnsafeStyles} from './style-utils' with {type: 'macro'};
import {style} from '../style' with {type: 'macro'};
import {StyleString} from '../style/types';

Expand Down Expand Up @@ -77,7 +77,7 @@ export function Provider(props: ProviderProps): JSX.Element {
generateDefaultColorSchemeStyles();

let providerStyles = style({
...colorScheme(),
...setColorScheme(),
'--s2-container-bg': {
type: 'backgroundColor',
value: {
Expand Down
4 changes: 2 additions & 2 deletions packages/@react-spectrum/s2/src/TableView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ import {
Virtualizer
} from 'react-aria-components';
import {ButtonGroup} from './ButtonGroup';
import {centerPadding, colorScheme, controlFont, getAllowedOverrides, StylesPropWithHeight, UnsafeStyles} from './style-utils' with {type: 'macro'};
import {centerPadding, controlFont, getAllowedOverrides, setColorScheme, StylesPropWithHeight, UnsafeStyles} from './style-utils' with {type: 'macro'};
import {Checkbox} from './Checkbox';
import Checkmark from '../s2wf-icons/S2_Icon_Checkmark_20_N.svg';
import Chevron from '../ui-icons/Chevron';
Expand Down Expand Up @@ -1103,7 +1103,7 @@ const editableCell = style<CellRenderProps & S2TableProps & {isDivider: boolean,
});

let editPopover = style({
...colorScheme(),
...setColorScheme(),
'--s2-container-bg': {
type: 'backgroundColor',
value: 'layer-2'
Expand Down
4 changes: 2 additions & 2 deletions packages/@react-spectrum/s2/src/Tooltip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import {
TooltipRenderProps,
useLocale
} from 'react-aria-components';
import {centerPadding, colorScheme, UnsafeStyles} from './style-utils' with {type: 'macro'};
import {centerPadding, setColorScheme, UnsafeStyles} from './style-utils' with {type: 'macro'};
import {ColorScheme} from '@react-types/provider';
import {ColorSchemeContext} from './Provider';
import {createContext, forwardRef, MutableRefObject, ReactNode, useCallback, useContext, useState} from 'react';
Expand All @@ -44,7 +44,7 @@ export interface TooltipProps extends Omit<AriaTooltipProps, 'children' | 'class
}

const tooltip = style<TooltipRenderProps & {colorScheme: ColorScheme | 'light dark' | null}>({
...colorScheme(),
...setColorScheme(),
justifyContent: 'center',
alignItems: 'center',
maxWidth: 160,
Expand Down
19 changes: 19 additions & 0 deletions packages/@react-spectrum/s2/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,14 @@ export {TreeView, TreeViewItem, TreeViewItemContent, TreeViewLoadMoreItem} from

export {pressScale} from './pressScale';

export {
getAllowedOverrides,
centerPadding,
setColorScheme
} from './style-utils';

export {mergeStyles} from '../style/runtime';

export {Autocomplete, Collection, FileTrigger, parseColor, useLocale} from 'react-aria-components';
export {useListData, useTreeData, useAsyncList} from 'react-stately';

Expand Down Expand Up @@ -168,3 +176,14 @@ export type {TooltipProps} from './Tooltip';
export type {TreeViewProps, TreeViewItemProps, TreeViewItemContentProps, TreeViewLoadMoreItemProps} from './TreeView';
export type {AutocompleteProps, FileTriggerProps, TooltipTriggerComponentProps as TooltipTriggerProps, SortDescriptor, Color, Key, Selection, RouterConfig} from 'react-aria-components';
export type {ListData, TreeData, AsyncListData} from 'react-stately';

export type {
StylesProp,
StylesPropWithHeight,
StylesPropWithoutWidth,
UnsafeClassName,
UnsafeStyles,
StyleProps,
WidthProperties,
HeightProperties
} from './style-utils';
58 changes: 57 additions & 1 deletion packages/@react-spectrum/s2/src/style-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,21 @@ import {CSSProperties} from 'react';
import {fontRelative} from '../style';
import {StyleString} from '../style/types';

/**
* Calculates vertical padding to center a single line of text within a container.
* Uses the CSS `self()` function and `1lh` unit to compute the padding based on
* the container's minimum height and border widths.
*
* @param minHeight - A CSS expression for the minimum height to center within. Defaults to `'self(minHeight)'`.
* @returns A CSS `calc()` expression wrapped as an arbitrary style value.
*
* @example
* ```tsx
* const styles = style({
* paddingY: centerPadding()
* });
* ```
*/
export function centerPadding(minHeight: string = 'self(minHeight)'): `[${string}]` {
return `[calc((${minHeight} - self(borderTopWidth, 0px) - self(borderBottomWidth, 0px) - 1lh) / 2)]`;
}
Expand Down Expand Up @@ -113,7 +128,20 @@ export const fieldInput = () => ({
containIntrinsicWidth: 'calc(var(--defaultWidth) - self(paddingStart, 0px) - self(paddingEnd, 0px) - self(borderStartWidth, 0px) - self(borderEndWidth, 0px))'
} as const);

export const colorScheme = () => ({
/**
* Returns style properties that set the CSS `color-scheme` for a component.
* Defaults to the page's color scheme and supports `'light'`, `'dark'`, and `'light dark'` values
* via the `colorScheme` render prop condition.
*
* @example
* ```tsx
* const styles = style({
* ...setColorScheme(),
* backgroundColor: 'layer-1'
* });
* ```
*/
export const setColorScheme = () => ({
colorScheme: {
// Default to page color scheme if none is defined.
default: '[var(--lightningcss-light, light) var(--lightningcss-dark, dark)]',
Expand Down Expand Up @@ -312,13 +340,19 @@ export const widthProperties = [
'maxWidth'
] as const;

/** The set of width-related CSS property names (`width`, `minWidth`, `maxWidth`). */
export type WidthProperties = (typeof widthProperties)[number];

export const heightProperties = [
'size',
'height',
'minHeight',
'maxHeight'
] as const;

/** The set of height-related CSS property names (`size`, `height`, `minHeight`, `maxHeight`). */
export type HeightProperties = (typeof heightProperties)[number];

export type StylesProp = StyleString<(typeof allowedOverrides)[number] | (typeof widthProperties)[number]>;
export type StylesPropWithHeight = StyleString<(typeof allowedOverrides)[number] | (typeof widthProperties)[number] | (typeof heightProperties)[number]>;
export type StylesPropWithoutWidth = StyleString<(typeof allowedOverrides)[number]>;
Expand All @@ -335,6 +369,28 @@ export interface StyleProps extends UnsafeStyles {
styles?: StylesProp
}

/**
* Returns the list of CSS property names that are allowed as style overrides via the `styles` prop.
Copy link
Member

Choose a reason for hiding this comment

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

This one is subject to breaking changes for any added or removed property.

I was originally thinking it might be better to turn the three
allowedOverrides, widthProperties, heightProperties
into macros so people can construct their own getAllowedOverrides, then if we need to change the list, we can still do that inside getAllowedOverrides

However, I realised that this is already subject to breaking changes through our own components to a degree. We cannot remove any. We can safely add them at the moment though since TS will prevent someone from using one that isn't allowed.

So if we add any properties in the future, maybe we have to wrap getAllowedOverrides in a new macro which appends more fields so we don't change this one.

* By default includes layout properties (margin, position, grid, etc.) and width properties.
* Optionally includes height properties.
*
* @param options - Configuration for which property groups to include.
* @param options.width - Whether to include width properties (`width`, `minWidth`, `maxWidth`). Defaults to `true`.
* @param options.height - Whether to include height properties (`height`, `minHeight`, `maxHeight`, `size`). Defaults to `false`.
* @returns An array of allowed CSS property names.
*
* @example
* ```tsx
* const styles = style({
* // ... component styles
* }, getAllowedOverrides());
*
* // With height overrides enabled:
* const styles = style({
* // ... component styles
* }, getAllowedOverrides({height: true}));
* ```
*/
export function getAllowedOverrides({width = true, height = false} = {}): string[] {
return (allowedOverrides as unknown as string[]).concat(width ? widthProperties : []).concat(height ? heightProperties : []);
}
60 changes: 59 additions & 1 deletion packages/@react-spectrum/s2/style/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,60 @@ import type {MacroContext} from '@parcel/macros';
import {StyleString} from './types';

export {baseColor, color, lightDark, colorMix, size, style} from './spectrum-theme';
export {raw, keyframes} from './style-macro';
export type {StyleString} from './types';

// Wrap these functions in arbitrary value syntax when called from the outside.
/**
* Converts a pixel value to a Spectrum spacing token in `rem` units.
*
* @param px - The spacing in pixels.
* @returns A `rem` value wrapped as an arbitrary style value.
*
* @example
* ```tsx
* import {space} from '@react-spectrum/s2/style' with {type: 'macro'};
*
* const styles = style({
* gap: space(12)
* });
* ```
*/
export function space(px: number): `[${string}]` {
return `[${internalSpace(px)}]`;
}

/**
* Converts a pixel value to a font-relative `em` length. Useful for sizing elements
* relative to the current font size.
*
* @param base - The pixel value to convert.
* @param baseFontSize - The base font size in pixels to divide by. Defaults to `14`.
* @returns A CSS `em` value wrapped as an arbitrary style value.
*
* @example
* ```tsx
* import {fontRelative} from '@react-spectrum/s2/style' with {type: 'macro'};
*
* const styles = style({
* gap: fontRelative(2) // 2/14 = ~0.143em
* });
* ```
*/
export function fontRelative(base: number, baseFontSize?: number): `[${string}]` {
return `[${internalFontRelative(base, baseFontSize)}]`;
}

/**
* Returns consistent Spectrum focus ring outline styles for interactive components.
Copy link
Member

Choose a reason for hiding this comment

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

we override the results of this one a *lot
I don't think we should export it until we consolidate those into an easier to use macro. Otherwise we semi-lock ourselves into the current implementation and we'd need to build a wrapper macro function

a handful of examples:



Copy link
Member

Choose a reason for hiding this comment

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

just realised, we already export this don't we... womp womp, we should probably build that wrapper around it for ourselves

*
* @example
* ```tsx
* const styles = style({
* ...focusRing(),
* borderRadius: 'lg'
* });
* ```
*/
export const focusRing = () => ({
outlineStyle: {
default: 'none',
Expand Down Expand Up @@ -78,6 +121,21 @@ const iconSizes = {
XL: 26
} as const;

/**
* Generates styles for an icon element with the given size, color, and layout options.
* Must be imported with `{type: 'macro'}`.
*
* @param options - Icon styling options including `size` (XS–XL), `color`, and layout properties.
* @returns A `StyleString` that can be applied to an icon element.
*
* @example
* ```tsx
* import {iconStyle} from '@react-spectrum/s2/style' with {type: 'macro'};
* import Edit from '@react-spectrum/s2/icons/Edit';
*
* <Edit styles={iconStyle({size: 'XL', color: 'positive'})} />
* ```
*/
export function iconStyle(this: MacroContext | void, options: IconStyle): StyleString<Exclude<keyof IconStyle, 'color' | 'size'>> {
let {size = 'M', color, ...styles} = options;

Expand Down
15 changes: 15 additions & 0 deletions packages/@react-spectrum/s2/style/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,21 @@ import {StyleString} from './types';
// };
// }

/**
* Merges multiple style strings together, combining the CSS properties from each.
* Later styles take precedence over earlier ones for the same property.
* Useful for composing styles from multiple `style()` macro calls.
*
* @example
* ```tsx
* import {mergeStyles} from '@react-spectrum/s2';
* import {style} from '@react-spectrum/s2/style' with {type: 'macro'};
*
* const baseStyles = style({padding: 8});
* const overrideStyles = style({padding: 16, color: 'heading'});
* const merged = mergeStyles(baseStyles, overrideStyles);
* ```
*/
export function mergeStyles(...styles: (StyleString | null | undefined)[]): StyleString {
let definedStyles = styles.filter(Boolean) as StyleString[];
if (definedStyles.length === 1) {
Expand Down
Loading