Beta Preview

Styling

React Spectrum includes a build-time style macro that generates atomic CSS and lets you apply Spectrum tokens directly in your components with type-safe autocompletion.

Style macro

The style macro runs at build time and returns a class name for applying Spectrum 2 design tokens (colors, spacing, sizing, typography, etc.). As can been seen below, the keys of the object passed to the style macro correspond to a CSS property, each paired with the property's desired value. See here for a full list of supported values.

import {style} from '@react-spectrum/s2/style' with {type: 'macro'};

<div className={style({backgroundColor: 'red-400', color: 'white'})}>
  {/* ... */}
</div>

Atomic output keeps your bundle small and scales well as your app grows. Each property/value pair is emitted once and reused everywhere.

.bJ { background-color: #ffbcb4 }
.ac { color: #fff }

Colocating styles with your component code means:

  • Develop more efficiently – no switching files or writing selectors.
  • Refactor with confidence – changes are isolated; deleting a component removes its styles.

Spectrum components

The styles prop accepts a limited set of CSS properties, including layout, spacing, sizing, and positioning. Other styles such as colors and internal padding cannot be customized within Spectrum components.

import {style} from '@react-spectrum/s2/style' with {type: 'macro'};
import {Button} from '@react-spectrum/s2';

<Button styles={style({marginStart: 8})}>Edit</Button>

Supported CSS properties

  • margin
  • marginStart
  • marginEnd
  • marginTop
  • marginBottom
  • marginX
  • marginY
  • width
  • minWidth
  • maxWidth
  • flexGrow
  • flexShrink
  • flexBasis
  • justifySelf
  • alignSelf
  • order
  • gridArea
  • gridRow
  • gridRowStart
  • gridRowEnd
  • gridColumn
  • gridColumnStart
  • gridColumnEnd
  • position
  • zIndex
  • top
  • bottom
  • inset
  • insetX
  • insetY
  • insetStart
  • insetEnd
  • visibility

Conditional styles

Define conditional values as objects to handle media queries, UI states (hover/press), and variants. This keeps all values for a property together.

<div
  className={style({
    padding: {
      default: 8,
      lg: 32,
      '@media (min-width: 2560px)': 64
    }
  })}
/>

In the example above, the keys of the nested object now map out the "conditions" that govern the padding of the div. This translates to the following:

  • If the viewport is larger than 2560px, as specified by a user defined media query, the padding of the div is set to 64px.
  • If the viewport matches the style macro's predefined lg breakpoint (i.e. the viewport is larger than 1024px), but does not exceed previous condition, the padding of the div is set to 32px
  • Otherwise, default to a padding of 8px.

Conditions are mutually exclusive and ordered. The macro uses CSS cascade layers so the last matching condition wins without specificity issues.

Runtime conditions

When runtime conditions are detected (e.g., variants, UI states), the macro returns a function to resolve styles at runtime.

import {style} from '@react-spectrum/s2/style' with {type: 'macro'};

const styles = style({
  backgroundColor: {
    variant: {
      primary: 'accent',
      secondary: 'neutral'
    }
  }
});

function MyComponent({variant}: {variant: 'primary' | 'secondary'}) {
  return <div className={styles({variant})} />
}

Boolean conditions starting with is or allows can be used directly without nesting:

const styles = style({
  backgroundColor: {
    default: 'gray-100',
    isSelected: 'gray-900',
    allowsRemoving: 'gray-400'
  }
});

<div className={styles({isSelected: true})} />

Runtime conditions work well with render props in React Aria Components. If you inline styles, you’ll get autocomplete for available conditions.

import {Checkbox} from 'react-aria-components';
import {style} from '@react-spectrum/s2/style' with {type: 'macro'};

<Checkbox
  className={style({
    backgroundColor: {
      default: 'gray-100',
      isHovered: 'gray-200',
      isSelected: 'gray-900'
    }
  })}
/>

Nesting conditions

Nest conditions to apply styles when multiple conditions are true. Conditions at the same level are mutually exclusive; order determines precedence.

const styles = style({
  backgroundColor: {
    default: 'gray-25',
    isSelected: {
      default: 'neutral',
      isEmphasized: 'accent',
      forcedColors: 'Highlight',
      isDisabled: {
        default: 'gray-400',
        forcedColors: 'GrayText'
      }
    }
  }
});

<div className={styles({isSelected, isEmphasized, isDisabled})} />

Reusing styles

Extract common styles into constants and spread them into style calls. These must be in the same file or imported from another file as a macro.

// style-utils.ts
export const bannerBackground = () => 'blue-1000' as const;

// component.tsx
import {bannerBackground} from './style-utils' with {type: 'macro'};
const horizontalStack = {
  display: 'flex',
  alignItems: 'center',
  columnGap: 8
} as const;

const styles = style({
  ...horizontalStack,
  backgroundColor: bannerBackground(),
  columnGap: 4
});

Create custom utilities by defining your own macros.

// style-utils.ts
export function horizontalStack(gap: number) {
  return {
    display: 'flex',
    alignItems: 'center',
    columnGap: gap
  } as const;
}

Usage:

// component.tsx
import {horizontalStack} from './style-utils' with {type: 'macro'};
import {style} from '@react-spectrum/s2/style' with {type: 'macro'};

const styles = style({
  ...horizontalStack(4),
  backgroundColor: 'base'
});

Built-in utilities

Use focusRing() to add the standard Spectrum focus ring.

import {style, focusRing} from '@react-spectrum/s2/style' with {type: 'macro'};
import {Button} from '@react-spectrum/s2';

const buttonStyle = style({
  ...focusRing(),
  // ...other styles
});

<Button styles={buttonStyle}>Press me</Button>

Setting CSS variables

CSS variables can be directly defined in a style macro, allowing child elements to then access them in their own styles. A type should be provided to specify the CSS property type the value represents.

const parentStyle = style({
  '--rowBackgroundColor': {
    type: 'backgroundColor',
    value: 'gray-400'
  }
});

const childStyle = style({
  backgroundColor: '--rowBackgroundColor'
});

Creating custom components

In-depth examples are coming soon!

mergeStyles can be used to merge the style strings from multiple macros together, with the latter styles taking precedence similar to object spreading. This behavior can be leveraged to create a component API that allows an end user to only override specific styles conditionally.

// User can override the component's background color ONLY if it isn't "quiet"
const baselineStyles = style({backgroundColor: 'gray-100'}, ['backgroundColor']);
const quietStyles = style({backgroundColor: 'transparent'});
const userStyles = style({backgroundColor: 'celery-600'});

function MyComponent({isQuiet, styles}: {isQuiet?: boolean, styles?: StyleString}) {
  let result = mergeStyles(
    baselineStyles(null, styles),
    isQuiet ? quietStyles : null
  );

  return <div className={result}>My component</div>
}

// Displays quiet styles
<MyComponent isQuiet styles={userStyles} />

// Displays user overrides
<MyComponent styles={userStyles} />

The iconStyle macro should be used when styling Icons, see the docs for more information.

CSS optimization

The style macro relies on CSS bundling and minification for optimal output. Follow these best practices:

  • Ensure styles are extracted into a CSS bundle; do not inject at runtime with <style> tags.
  • Use a CSS minifier like lightningcss to deduplicate common rules (consider in dev for easier debugging).
  • Bundle all CSS for S2 components and style macros into a single CSS bundle rather than code splitting to avoid duplicate rules across chunks.

Failure to perform this optimization will result in a suboptimal developer experience and obtuse styling bugs.

Parcel

Parcel supports macros out of the box and optimizes CSS with Lightning CSS. You can bundle all S2 and macro CSS into a single file using manual shared bundles.

// package.json
{
  "@parcel/bundler-default": {
    "manualSharedBundles": [
      {
        "name": "s2-styles",
        "assets": [
          "**/@react-spectrum/s2/**",
          // Update this glob as needed to match your source files.
          "src/**/*.{js,jsx,ts,tsx}"
        ],
        "types": ["css"]
      }
    ]
  }
}

Webpack

See the webpack example for a full configuration.

Vite

See the Vite example for full configuration options.

CSS Resets

CSS resets are strongly discouraged. Global CSS selectors can unintentionally affect elements that were not intended to have their styles be modified, leading to style clashes. Since Spectrum 2 uses CSS Cascade Layers, global CSS outside a @layer will override S2's CSS. Therefore, if you cannot remove your CSS reset, it must be placed in a lower layer. This can be done by declaring your reset layer before the _ layer used by S2.

/* App.css */
@layer reset, _;
@import "reset.css" layer(reset);

Developing with style macros

Since style macros are quite different from using className/style directly, many may find it initially challenging to debug and develop against. Below are some useful tools that may benefit your developer experience:

  • The atomic-css-devtools extension presents an inspected element's atomic CSS rules in a non-atomic format, making it easier to scan.

  • This sandbox is preconfigured to support React Spectrum S2, React Aria Components, and the style macros for quick prototyping.

  • If you are using Cursor, we offer a set of Cursor rules to use when developing with style macros. Additionally, we have MCP servers for React Aria and React Spectrum respectively that interface with the docs.

FAQ