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
`}>
{/* ... */}
</Radio>
<Radio
className={({ isFocusVisible, isSelected }) => `
flex rounded-lg p-4
`}
>
{/* ... */}
</Radio>
<Radio
className={(
{
isFocusVisible,
isSelected
}
) => `
flex rounded-lg p-4
`}
>
{/* ... */}
</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>