Styling

React Aria Components do not include any styles by default, allowing you to build custom designs to fit your application or design system using any styling solution.

Class names#


Each component accepts the standard className and style props which enable using vanilla CSS, utility classes (e.g. Tailwind), CSS-in-JS (e.g. Styled Components), etc.

When a custom className is not provided, each component includes a default class name following the react-aria-ComponentName naming convention. You can use this to style a component with standard CSS without needing any custom classes.

.react-aria-Select {
  /* ... */
}
.react-aria-Select {
  /* ... */
}
.react-aria-Select {
  /* ... */
}

A custom className can also be specified on any component. This overrides the default className provided by React Aria with your own.

<Select className="my-select">
  {/* ... */}
</Select>
<Select className="my-select">
  {/* ... */}
</Select>
<Select className="my-select">
  {/* ... */}
</Select>

The default class names for each component are listed in the Styling section of their documentation.

States#


Components often support multiple UI states (e.g. pressed, hovered, selected, etc.). React Aria Components exposes states using data attributes, which you can target in CSS selectors. They can be thought of like custom CSS pseudo classes. For example:

.react-aria-ListBoxItem[data-selected] {
  /* ... */
}

.react-aria-ListBoxItem[data-focused] {
  /* ... */
}
.react-aria-ListBoxItem[data-selected] {
  /* ... */
}

.react-aria-ListBoxItem[data-focused] {
  /* ... */
}
.react-aria-ListBoxItem[data-selected] {
  /* ... */
}

.react-aria-ListBoxItem[data-focused] {
  /* ... */
}

In order to ensure high quality interactions across browsers and devices, React Aria Components includes states such as data-hovered and data-pressed which are similar to CSS pseudo classes such as :hover and :active, but work consistently between mouse, touch, and keyboard modalities. You can read more about this in our blog post series and our Interactions overview.

All states supported by each component are listed in the Styling section of their documentation.

Render props#


The className and style props also accept functions which receive states for styling. This lets you dynamically determine the classes or styles to apply.

<ListBoxItem
  className={({ isSelected }) => isSelected ? 'selected' : 'unselected'}
>
  Item
</ListBoxItem>
<ListBoxItem
  className={({ isSelected }) =>
    isSelected ? 'selected' : 'unselected'}
>
  Item
</ListBoxItem>
<ListBoxItem
  className={(
    { isSelected }
  ) =>
    isSelected
      ? 'selected'
      : 'unselected'}
>
  Item
</ListBoxItem>

Render props may also be used as children to alter what elements are rendered based on the current state. For example, you could render a checkmark icon when an item is selected.

<ListBoxItem>
  {({isSelected}) => (
    <>
      {isSelected && <CheckmarkIcon />}
      <span>Item</span>
    </>
  )}
</ListBoxItem>
<ListBoxItem>
  {({isSelected}) => (
    <>
      {isSelected && <CheckmarkIcon />}
      <span>Item</span>
    </>
  )}
</ListBoxItem>
<ListBoxItem>
  {(
    { isSelected }
  ) => (
    <>
      {isSelected && (
        <CheckmarkIcon />
      )}
      <span>Item</span>
    </>
  )}
</ListBoxItem>

The render props exposed for each component are listed in the Styling section of their documentation.

Slots#


Some components include multiple instances of the same component as children. These use the slot prop to distinguish them, which can also be used in CSS for styling purposes. This example targets the increment and decrement buttons in a NumberField.

<NumberField>
  <Label>Width</Label>
  <Group>
    <Input />
    <Button slot="increment">+</Button>
    <Button slot="decrement">-</Button>  </Group>
</NumberField>
<NumberField>
  <Label>Width</Label>
  <Group>
    <Input />
    <Button slot="increment">+</Button>
    <Button slot="decrement">-</Button>  </Group>
</NumberField>
<NumberField>
  <Label>Width</Label>
  <Group>
    <Input />
    <Button slot="increment">
      +
    </Button>
    <Button slot="decrement">
      -
    </Button>  </Group>
</NumberField>
.react-aria-NumberField {
  [slot=increment] {     border-radius: 4px 4px 0 0;
  }

  [slot=decrement] {     border-radius: 0 0 4px 4px;
  }
}
.react-aria-NumberField {
  [slot=increment] {     border-radius: 4px 4px 0 0;
  }

  [slot=decrement] {     border-radius: 0 0 4px 4px;
  }
}
.react-aria-NumberField {
  [slot=increment] {     border-radius: 4px 4px 0 0;
  }

  [slot=decrement] {     border-radius: 0 0 4px 4px;
  }
}

The slots supported by each component are shown in the Anatomy section of their documentation.

CSS variables#


Some components provide CSS variables that you can use in your styling code. For example, the Select component provides a --trigger-width variable on the popover that is set to the width of the trigger button. You can use this to make the width of the popover match the width of the button.

.react-aria-Popover {
  width: var(--trigger-width);
}
.react-aria-Popover {
  width: var(--trigger-width);
}
.react-aria-Popover {
  width: var(--trigger-width);
}

The CSS variables provided by each component are listed in the Styling section of their documentation.

Tailwind CSS#


Tailwind CSS is a utility-first CSS framework for rapid styling that works great with React Aria Components. To access states, you can use data attributes as modifiers:

<ListBoxItem className="data-[selected]:bg-blue-400 data-[disabled]:bg-gray-100">
  Item
</ListBoxItem>
<ListBoxItem className="data-[selected]:bg-blue-400 data-[disabled]:bg-gray-100">
  Item
</ListBoxItem>
<ListBoxItem className="data-[selected]:bg-blue-400 data-[disabled]:bg-gray-100">
  Item
</ListBoxItem>

Alternatively, you can use render props to control which Tailwind classes are applied based on states. This can be useful if you need to apply multiple classes based on a single state:

<Radio
  className={({isFocusVisible, isSelected}) => `
    flex rounded-lg p-4
    ${isFocusVisible ? 'ring-2 ring-blue-600 ring-offset-1' : ''}
    ${isSelected ? 'bg-blue-600 border-white/30 text-white' : ''}
  `}>
  {/* ... */}
</Radio>
<Radio
  className={({ isFocusVisible, isSelected }) => `
  flex rounded-lg p-4
  ${
    isFocusVisible
      ? 'ring-2 ring-blue-600 ring-offset-1'
      : ''
  }
  ${
    isSelected
      ? 'bg-blue-600 border-white/30 text-white'
      : ''
  }
`}
>
  {/* ... */}
</Radio>
<Radio
  className={(
    {
      isFocusVisible,
      isSelected
    }
  ) => `
  flex rounded-lg p-4
  ${
    isFocusVisible
      ? 'ring-2 ring-blue-600 ring-offset-1'
      : ''
  }
  ${
    isSelected
      ? 'bg-blue-600 border-white/30 text-white'
      : ''
  }
`}
>
  {/* ... */}
</Radio>

To access CSS variables, use Tailwind's arbitrary value syntax.

<Popover className="w-[--trigger-width]">
  {/* ... */}
</Popover>
<Popover className="w-[--trigger-width]">
  {/* ... */}
</Popover>
<Popover className="w-[--trigger-width]">
  {/* ... */}
</Popover>

Plugin#

A Tailwind CSS plugin is also available to make styling states of React Aria Components easier, with shorter names and autocomplete in your editor. To install:

yarn add tailwindcss-react-aria-components

Then add the plugin to your tailwind.config.js file:

/** @type {import('tailwindcss').Config} */
module.exports = {
  plugins: [
    require('tailwindcss-react-aria-components')
  ]
};
/** @type {import('tailwindcss').Config} */
module.exports = {
  plugins: [
    require('tailwindcss-react-aria-components')
  ]
};
/** @type {import('tailwindcss').Config} */
module.exports = {
  plugins: [
    require(
      'tailwindcss-react-aria-components'
    )
  ]
};

With the plugin installed, you can now access all states without the data- prefix. If you have the Tailwind VSCode Extension installed, you'll also get autocomplete for all states in your editor.

<ListBoxItem className="selected:bg-blue-400 disabled:bg-gray-100">
  Item
</ListBoxItem>
<ListBoxItem className="selected:bg-blue-400 disabled:bg-gray-100">
  Item
</ListBoxItem>
<ListBoxItem className="selected:bg-blue-400 disabled:bg-gray-100">
  Item
</ListBoxItem>

Boolean states#

Boolean states such as data-pressed can be styled with pressed: like this:

<Button className="pressed:bg-blue">
  {/* ... */}
</Button>
<Button className="pressed:bg-blue">
  {/* ... */}
</Button>
<Button className="pressed:bg-blue">
  {/* ... */}
</Button>

Non-boolean states#

Non-boolean states follow the {name}-{value} pattern, so you can style an element with data-orientation="vertical" using orientation-vertical:.

<Tabs className="orientation-vertical:flex-row">
  {/* ... */}
</Tabs>
<Tabs className="orientation-vertical:flex-row">
  {/* ... */}
</Tabs>
<Tabs className="orientation-vertical:flex-row">
  {/* ... */}
</Tabs>

Modifier prefix#

By default, all modifiers are unprefixed (e.g. disabled:), and generate CSS that automatically handles both React Aria Components and native CSS pseudo classes when the names conflict. If you prefer, you can optionally prefix all React Aria Components modifiers with a string of your choice.

/** @type {import('tailwindcss').Config} */
module.exports = {
  plugins: [
    require('tailwindcss-react-aria-components')({prefix: 'rac'})
  ],
};
/** @type {import('tailwindcss').Config} */
module.exports = {
  plugins: [
    require('tailwindcss-react-aria-components')({
      prefix: 'rac'
    })
  ]
};
/** @type {import('tailwindcss').Config} */
module.exports = {
  plugins: [
    require(
      'tailwindcss-react-aria-components'
    )({ prefix: 'rac' })
  ]
};

With this configured, all states for React Aria Components can be accessed with that prefix.

<ListBoxItem className="rac-selected:bg-blue-400 rac-disabled:bg-gray-100">
  Item
</ListBoxItem>
<ListBoxItem className="rac-selected:bg-blue-400 rac-disabled:bg-gray-100">
  Item
</ListBoxItem>
<ListBoxItem className="rac-selected:bg-blue-400 rac-disabled:bg-gray-100">
  Item
</ListBoxItem>

Animation#


React Aria Components support CSS keyframe animations, and work with JavaScript animation libraries like Framer Motion.

CSS animations#

Overlay components such as Popover and Modal support entry and exit animations via the [data-entering] and [data-exiting] states, or via the corresponding render prop functions. You can use these states to apply CSS keyframe animations. These components will automatically wait for any exit animations to complete before they are removed from the DOM.

.react-aria-Popover[data-entering] {
  animation: slide 300ms;
}

.react-aria-Popover[data-exiting] {
  animation: slide 300ms reverse;
}

@keyframes slide {
  from {
    transform: translateY(-20px);
    opacity: 0;
  }

  to {
    transform: translateY(0);
    opacity: 1;
  }
}
.react-aria-Popover[data-entering] {
  animation: slide 300ms;
}

.react-aria-Popover[data-exiting] {
  animation: slide 300ms reverse;
}

@keyframes slide {
  from {
    transform: translateY(-20px);
    opacity: 0;
  }

  to {
    transform: translateY(0);
    opacity: 1;
  }
}
.react-aria-Popover[data-entering] {
  animation: slide 300ms;
}

.react-aria-Popover[data-exiting] {
  animation: slide 300ms reverse;
}

@keyframes slide {
  from {
    transform: translateY(-20px);
    opacity: 0;
  }

  to {
    transform: translateY(0);
    opacity: 1;
  }
}

Tailwind CSS#

If you are using Tailwind CSS, we recommend using the tailwindcss-animate plugin. This includes utilities for building common animations such as fading, sliding, and zooming.

<Popover className="data-[entering]:animate-in data-[entering]:fade-in data-[exiting]:animate-out data-[exiting]:fade-out">
  {/* ... */}
</Popover>
<Popover className="data-[entering]:animate-in data-[entering]:fade-in data-[exiting]:animate-out data-[exiting]:fade-out">
  {/* ... */}
</Popover>
<Popover className="data-[entering]:animate-in data-[entering]:fade-in data-[exiting]:animate-out data-[exiting]:fade-out">
  {/* ... */}
</Popover>

Framer Motion#

Framer Motion and other JavaScript animation libraries can also be used with React Aria Components. Use the motion function to create a wrapper component that adds support for Framer Motion's animation props.

import {Modal, ModalOverlay} from 'react-aria-components';
import {motion} from 'framer-motion';

// Create Framer Motion wrappers.
const MotionModal = motion(Modal);
const MotionModalOverlay = motion(ModalOverlay);
import {Modal, ModalOverlay} from 'react-aria-components';
import {motion} from 'framer-motion';

// Create Framer Motion wrappers.
const MotionModal = motion(Modal);
const MotionModalOverlay = motion(ModalOverlay);
import {
  Modal,
  ModalOverlay
} from 'react-aria-components';
import {motion} from 'framer-motion';

// Create Framer Motion wrappers.
const MotionModal =
  motion(Modal);
const MotionModalOverlay =
  motion(ModalOverlay);

This enables using props like animate with React Aria Components.

<MotionModal
  initial={{opacity: 0}}
  animate={{opacity: 1}}>
  {/* ... */}
</MotionModal>
<MotionModal
  initial={{opacity: 0}}
  animate={{opacity: 1}}>
  {/* ... */}
</MotionModal>
<MotionModal
  initial={{opacity: 0}}
  animate={{opacity: 1}}>
  {/* ... */}
</MotionModal>

Overlay exit animations can be implemented using the isExiting prop, which keeps the element in the DOM until an animation is complete. Framer Motion's variants are a good way to setup named animation states.

type AnimationState = 'unmounted' | 'hidden' | 'visible';

function Example() {
  // Track animation state.
  let [animation, setAnimation] = React.useState<AnimationState>('unmounted');
  return (
    <DialogTrigger
      // Start animation when open state changes.
      onOpenChange={(isOpen) => setAnimation(isOpen ? 'visible' : 'hidden')}    >
      <Button>Open dialog</Button>
      <MotionModalOverlay
        // Prevent modal from unmounting during animation.
        isExiting={animation === 'hidden'}
        // Reset animation state once it is complete.
        onAnimationComplete={(animation) => {
          setAnimation((a) =>
            animation === 'hidden' && a === 'hidden' ? 'unmounted' : a
          );
        }}        variants={{
          hidden: { opacity: 0 },
          visible: { opacity: 1 }
        }}
        initial="hidden"
        animate={animation}
      >
        <MotionModal
          variants={{
            hidden: { opacity: 0, y: 32 },
            visible: { opacity: 1, y: 0 }
          }}
        >
          {/* ... */}
        </MotionModal>
      </MotionModalOverlay>
    </DialogTrigger>
  );
}
type AnimationState = 'unmounted' | 'hidden' | 'visible';

function Example() {
  // Track animation state.
  let [animation, setAnimation] = React.useState<
    AnimationState
  >('unmounted');
  return (
    <DialogTrigger
      // Start animation when open state changes.
      onOpenChange={(isOpen) =>
        setAnimation(isOpen ? 'visible' : 'hidden')}    >
      <Button>Open dialog</Button>
      <MotionModalOverlay
        // Prevent modal from unmounting during animation.
        isExiting={animation === 'hidden'}
        // Reset animation state once it is complete.
        onAnimationComplete={(animation) => {
          setAnimation((a) =>
            animation === 'hidden' && a === 'hidden'
              ? 'unmounted'
              : a
          );
        }}        variants={{
          hidden: { opacity: 0 },
          visible: { opacity: 1 }
        }}
        initial="hidden"
        animate={animation}
      >
        <MotionModal
          variants={{
            hidden: { opacity: 0, y: 32 },
            visible: { opacity: 1, y: 0 }
          }}
        >
          {/* ... */}
        </MotionModal>
      </MotionModalOverlay>
    </DialogTrigger>
  );
}
type AnimationState =
  | 'unmounted'
  | 'hidden'
  | 'visible';

function Example() {
  // Track animation state.
  let [
    animation,
    setAnimation
  ] = React.useState<
    AnimationState
  >('unmounted');
  return (
    <DialogTrigger
      // Start animation when open state changes.
      onOpenChange={(isOpen) =>
        setAnimation(
          isOpen
            ? 'visible'
            : 'hidden'
        )}    >
      <Button>
        Open dialog
      </Button>
      <MotionModalOverlay
        // Prevent modal from unmounting during animation.
        isExiting={animation ===
          'hidden'}
        // Reset animation state once it is complete.
        onAnimationComplete={(animation) => {
          setAnimation(
            (a) =>
              animation ===
                  'hidden' &&
                a ===
                  'hidden'
                ? 'unmounted'
                : a
          );
        }}        variants={{
          hidden: {
            opacity: 0
          },
          visible: {
            opacity: 1
          }
        }}
        initial="hidden"
        animate={animation}
      >
        <MotionModal
          variants={{
            hidden: {
              opacity: 0,
              y: 32
            },
            visible: {
              opacity: 1,
              y: 0
            }
          }}
        >
          {/* ... */}
        </MotionModal>
      </MotionModalOverlay>
    </DialogTrigger>
  );
}

Note: Framer Motion's AnimatePresence component may not work with React Aria overlays in all cases, so the example shown above is the recommended approach for exit animations.

The AnimatePresence component allows you to animate when items are added or removed in collection components. Use array.map to create children, and make sure each child has a unique key in addition to an id to ensure Framer Motion can track it.

import {GridList, GridListItem} from 'react-aria-components';
import {motion, AnimatePresence} from 'framer-motion';

const MotionItem = motion(GridListItem);

<GridList>
  <AnimatePresence>
    {items.map(item => (
      <MotionItem
        key={item.id}
        id={item.id}
        layout
        exit={{opacity: 0}}>
        {/* ... */}
      </MotionItem>
    ))}
  </AnimatePresence>
</GridList>
import {
  GridList,
  GridListItem
} from 'react-aria-components';
import {AnimatePresence, motion} from 'framer-motion';

const MotionItem = motion(GridListItem);

<GridList>
  <AnimatePresence>
    {items.map((item) => (
      <MotionItem
        key={item.id}
        id={item.id}
        layout
        exit={{ opacity: 0 }}
      >
        {/* ... */}
      </MotionItem>
    ))}
  </AnimatePresence>
</GridList>
import {
  GridList,
  GridListItem
} from 'react-aria-components';
import {
  AnimatePresence,
  motion
} from 'framer-motion';

const MotionItem =
  motion(GridListItem);

<GridList>
  <AnimatePresence>
    {items.map(
      (item) => (
        <MotionItem
          key={item.id}
          id={item.id}
          layout
          exit={{
            opacity: 0
          }}
        >
          {/* ... */}
        </MotionItem>
      )
    )}
  </AnimatePresence>
</GridList>