W↓
All docs
🔑
Sign Up/Sign In
react-spectrum.adobe.com/react-aria/
Public Link
Jun 18, 2025, 7:05:35 AM - complete - 4 MB
Jun 18, 2025, 7:05:35 AM - complete - 4 MB
Jun 18, 2025, 6:55:07 AM - complete - 11.4 kB
Created by:
****ad@vlad.studio
Starting URLs:
https://react-spectrum.adobe.com/react-aria/getting-started.html
CSS Selector:
article
Crawl Prefixes:
https://react-spectrum.adobe.com/react-aria/
## Page: https://react-spectrum.adobe.com/react-aria/getting-started.html # Getting Started This page describes how to get started with React Aria. ## What is React Aria?# * * * **React Aria** is a library of unstyled React components and hooks that helps you build accessible, high quality UI components for your application or design system. It provides components for common UI patterns, with accessibility, internationalization, interactions, and behavior built in, allowing you to focus on your unique design and styling rather than re-building these challenging aspects. React Aria has been meticulously tested across a wide variety of devices, interaction modalities, and assistive technologies to ensure the best experience possible for all users. ## Installation# * * * React Aria Components can be installed using a package manager like npm or yarn. yarn add react-aria-components All components are available in this one package for ease of dependency management. ## Building a component# * * * Once installed, you can import and render the components you need. Each component may include several parts, as documented on the corresponding component page. The API is designed around composition, where each component generally has a 1:1 relationship with a single DOM element. This makes it easy to style every element, and control the layout and DOM order as needed to implement your design. This example renders a custom Select. import {Button, Label, ListBox, ListBoxItem, Popover, Select, SelectValue} from 'react-aria-components'; <Select> <Label>Favorite Animal</Label> <Button> <SelectValue /> <span aria-hidden="true">▼</span> </Button> <Popover> <ListBox> <ListBoxItem>Cat</ListBoxItem> <ListBoxItem>Dog</ListBoxItem> <ListBoxItem>Kangaroo</ListBoxItem> </ListBox> </Popover> </Select> import { Button, Label, ListBox, ListBoxItem, Popover, Select, SelectValue } from 'react-aria-components'; <Select> <Label>Favorite Animal</Label> <Button> <SelectValue /> <span aria-hidden="true">▼</span> </Button> <Popover> <ListBox> <ListBoxItem>Cat</ListBoxItem> <ListBoxItem>Dog</ListBoxItem> <ListBoxItem>Kangaroo</ListBoxItem> </ListBox> </Popover> </Select> import { Button, Label, ListBox, ListBoxItem, Popover, Select, SelectValue } from 'react-aria-components'; <Select> <Label> Favorite Animal </Label> <Button> <SelectValue /> <span aria-hidden="true"> ▼ </span> </Button> <Popover> <ListBox> <ListBoxItem> Cat </ListBoxItem> <ListBoxItem> Dog </ListBoxItem> <ListBoxItem> Kangaroo </ListBoxItem> </ListBox> </Popover> </Select> Favorite AnimalSelect an item▼ CatDogKangaroo ### Styling# React Aria Components do not include any styles by default, allowing you to build custom designs to fit your application or design system. It works with any styling solution, including vanilla CSS, Tailwind CSS, CSS-in-JS, etc. By default, all React Aria Components include CSS class names that you can use for styling, along with data attributes to represent states such as pressed, hovered, selected, etc. .react-aria-ListBoxItem { color: black; &[data-selected] { background: slateblue; color: white; } } .react-aria-ListBoxItem { color: black; &[data-selected] { background: slateblue; color: white; } } .react-aria-ListBoxItem { color: black; &[data-selected] { background: slateblue; color: white; } } This is a quick way to get started, but you can also create your own custom classes using the `className` prop, which overrides the defaults. The full CSS for all of the components in above example is below: Show CSS @import "@react-aria/example-theme"; .react-aria-Select { .react-aria-Button { color: var(--text-color); background: var(--button-background); border: 1px solid var(--border-color); box-shadow: 0 1px 2px rgba(0 0 0 / 0.1); forced-color-adjust: none; border-radius: 6px; appearance: none; vertical-align: middle; font-size: 1.072rem; padding: 0.286rem 0.286rem 0.286rem 0.571rem; margin: 0; outline: none; display: flex; align-items: center; max-width: 250px; &[data-focus-visible] { outline: 2px solid var(--focus-ring-color); outline-offset: -1px; } &[data-pressed] { background: var(--button-background-pressed); } } .react-aria-SelectValue { &[data-placeholder] { font-style: italic; color: var(--text-color-placeholder); } } span[aria-hidden] { width: 1.5rem; line-height: 1.375rem; margin-left: 1rem; padding: 1px; background: var(--highlight-background); color: var(--highlight-foreground); border-radius: 4px; font-size: 0.857rem; } } .react-aria-ListBox { max-height: inherit; box-sizing: border-box; overflow: auto; padding: 2px; outline: none; .react-aria-ListBoxItem { margin: 2px; padding: 0.286rem 0.571rem 0.286rem 1.571rem; border-radius: 6px; outline: none; cursor: default; color: var(--text-color); font-size: 1.072rem; position: relative; display: flex; flex-direction: column; forced-color-adjust: none; &[data-selected] { font-weight: 600; &::before { content: '✓'; content: '✓' / ''; alt: ' '; position: absolute; top: 4px; left: 4px; } } &[data-focused], &[data-pressed] { background: var(--highlight-background); color: var(--highlight-foreground); } } } .react-aria-Popover { border: 1px solid var(--gray-200); box-shadow: 0 8px 20px rgba(0 0 0 / 0.1); border-radius: 6px; background: var(--overlay-background); outline: none; min-width: var(--trigger-width); max-width: 250px; box-sizing: border-box; &[data-placement=top] { --origin: translateY(8px); } &[data-placement=bottom] { --origin: translateY(-8px); } &[data-entering] { animation: slide 200ms; } &[data-exiting] { animation: slide 200ms reverse ease-in; } } @keyframes slide { from { transform: var(--origin); opacity: 0; } to { transform: translateY(0); opacity: 1; } } @import "@react-aria/example-theme"; .react-aria-Select { .react-aria-Button { color: var(--text-color); background: var(--button-background); border: 1px solid var(--border-color); box-shadow: 0 1px 2px rgba(0 0 0 / 0.1); forced-color-adjust: none; border-radius: 6px; appearance: none; vertical-align: middle; font-size: 1.072rem; padding: 0.286rem 0.286rem 0.286rem 0.571rem; margin: 0; outline: none; display: flex; align-items: center; max-width: 250px; &[data-focus-visible] { outline: 2px solid var(--focus-ring-color); outline-offset: -1px; } &[data-pressed] { background: var(--button-background-pressed); } } .react-aria-SelectValue { &[data-placeholder] { font-style: italic; color: var(--text-color-placeholder); } } span[aria-hidden] { width: 1.5rem; line-height: 1.375rem; margin-left: 1rem; padding: 1px; background: var(--highlight-background); color: var(--highlight-foreground); border-radius: 4px; font-size: 0.857rem; } } .react-aria-ListBox { max-height: inherit; box-sizing: border-box; overflow: auto; padding: 2px; outline: none; .react-aria-ListBoxItem { margin: 2px; padding: 0.286rem 0.571rem 0.286rem 1.571rem; border-radius: 6px; outline: none; cursor: default; color: var(--text-color); font-size: 1.072rem; position: relative; display: flex; flex-direction: column; forced-color-adjust: none; &[data-selected] { font-weight: 600; &::before { content: '✓'; content: '✓' / ''; alt: ' '; position: absolute; top: 4px; left: 4px; } } &[data-focused], &[data-pressed] { background: var(--highlight-background); color: var(--highlight-foreground); } } } .react-aria-Popover { border: 1px solid var(--gray-200); box-shadow: 0 8px 20px rgba(0 0 0 / 0.1); border-radius: 6px; background: var(--overlay-background); outline: none; min-width: var(--trigger-width); max-width: 250px; box-sizing: border-box; &[data-placement=top] { --origin: translateY(8px); } &[data-placement=bottom] { --origin: translateY(-8px); } &[data-entering] { animation: slide 200ms; } &[data-exiting] { animation: slide 200ms reverse ease-in; } } @keyframes slide { from { transform: var(--origin); opacity: 0; } to { transform: translateY(0); opacity: 1; } } @import "@react-aria/example-theme"; .react-aria-Select { .react-aria-Button { color: var(--text-color); background: var(--button-background); border: 1px solid var(--border-color); box-shadow: 0 1px 2px rgba(0 0 0 / 0.1); forced-color-adjust: none; border-radius: 6px; appearance: none; vertical-align: middle; font-size: 1.072rem; padding: 0.286rem 0.286rem 0.286rem 0.571rem; margin: 0; outline: none; display: flex; align-items: center; max-width: 250px; &[data-focus-visible] { outline: 2px solid var(--focus-ring-color); outline-offset: -1px; } &[data-pressed] { background: var(--button-background-pressed); } } .react-aria-SelectValue { &[data-placeholder] { font-style: italic; color: var(--text-color-placeholder); } } span[aria-hidden] { width: 1.5rem; line-height: 1.375rem; margin-left: 1rem; padding: 1px; background: var(--highlight-background); color: var(--highlight-foreground); border-radius: 4px; font-size: 0.857rem; } } .react-aria-ListBox { max-height: inherit; box-sizing: border-box; overflow: auto; padding: 2px; outline: none; .react-aria-ListBoxItem { margin: 2px; padding: 0.286rem 0.571rem 0.286rem 1.571rem; border-radius: 6px; outline: none; cursor: default; color: var(--text-color); font-size: 1.072rem; position: relative; display: flex; flex-direction: column; forced-color-adjust: none; &[data-selected] { font-weight: 600; &::before { content: '✓'; content: '✓' / ''; alt: ' '; position: absolute; top: 4px; left: 4px; } } &[data-focused], &[data-pressed] { background: var(--highlight-background); color: var(--highlight-foreground); } } } .react-aria-Popover { border: 1px solid var(--gray-200); box-shadow: 0 8px 20px rgba(0 0 0 / 0.1); border-radius: 6px; background: var(--overlay-background); outline: none; min-width: var(--trigger-width); max-width: 250px; box-sizing: border-box; &[data-placement=top] { --origin: translateY(8px); } &[data-placement=bottom] { --origin: translateY(-8px); } &[data-entering] { animation: slide 200ms; } &[data-exiting] { animation: slide 200ms reverse ease-in; } } @keyframes slide { from { transform: var(--origin); opacity: 0; } to { transform: translateY(0); opacity: 1; } } Check out our styling guide to learn more about styling, states, render props, working with Tailwind CSS, and animation. ## Starter kits# * * * To help kick-start your project, we offer starter kits that include example implementations of all React Aria components with various styling solutions. All components are fully styled, including support for dark mode, high contrast mode, and all UI states. Each starter comes with a pre-configured Storybook that you can experiment with, or use as a starting point for your own component library. Vanilla CSS Download ZIP Preview Tailwind CSS Download ZIP Preview ## Hooks# * * * React Aria includes both components and hooks depending on the level of customization you need. The components provide a default DOM structure and styling API, and handle all of the glue code necessary to connect the hooks together on your behalf. If you need even more control, you can drop down to the lower-level hook-based API. Check out our Hooks Introduction to get started. The components and hooks also work together, allowing them to be mixed and matched depending on the level of customization required. We recommend starting with components, dropping down to hooks only when additional flexibility is needed. See our Advanced Customization guide to learn more about using components and hooks together. --- ## Page: https://react-spectrum.adobe.com/react-aria/why.html # Why React Aria? This page discusses why React Aria exists, and the problems it solves. ## Motivation# * * * Design systems are now more popular than ever, and many companies both large and small are implementing their own component libraries from scratch. Modern view libraries like React allow teams to build and maintain these components more easily than ever before, but it is still **extraordinarily difficult** to do so in a fully accessible way with interactions that work across many types of devices. The web is a very unique platform. It runs across a very wide variety of devices and interaction models, and because there is no well defined platform aesthetic, every company usually needs to style each component in their design system to match their brand. There are very few built-in UI controls in the browser, and those that do exist are very hard to style. This leads to web developers at every company needing to rebuild every control from scratch. This represents **millions of dollars** of investment for each company to **duplicate work** that many other companies are also doing. The fully styleable primitives that the web offers (e.g. `<div>`) are quite powerful, but they lack semantic meaning. This means that **accessibility is often missing** because assistive technology cannot make sense of the div soup that we use to implement our components. Even if you use ARIA to provide semantics, you still need to implement all of the behavior for each component from scratch using JavaScript, and this can be tricky to do across many devices and interaction models. Unfortunately, many companies and teams don't have the resources or time to prioritize features like accessibility, internationalization, full keyboard navigation, touch interactions, and more. This leads to many web apps having sub-par accessibility and interactions, and contributes to the perception of the web as an inferior app platform compared to native apps. ## React Aria# * * * React Aria separates the behavior and accessibility implementation for many common UI components out into unstyled React components and hooks, which enables them to be reused easily between design systems and applications. You remain in complete control over the rendering and styling aspects of your components, but get most of the behavior, accessibility, internationalization, and much more for free. Rather than starting with only divs and building everything yourself, you start with elements that have semantic meaning, behavior, and interactions built in. This allows you to **build components more quickly**, and ensures that they work well across mouse, touch, and keyboard interactions, as well as with screen readers and other assistive technology. ## Customization# * * * The main reason developers need to create their own component libraries from scratch is styling. Built-in browser UI controls are not customizable enough, and in many cases do not exist at all. React Aria does not include any styles by default, allowing you to build custom designs to fit your application or design system using any styling solution. React Aria components include built-in CSS class names, states, and render props to make styling a breeze. See our styling guide to learn more. Most components have behavior that is consistent even across design systems. The ARIA Authoring Practices Guide published by the W3C specifies the expected keyboard and accessibility behavior for most common components. In addition, touch and mouse behavior is usually quite consistent regardless of the design. A button is still a button, even if it looks a little different. React Aria offers a unique combination of high-level components that provide an easy to use API, and low-level hooks that enable advanced customization use cases. This allows you to customize the DOM structure, intercept events to implement custom behavior, override internal state management, and much more when you need to, while keeping things simple when you don't. Check out our advanced customization guide to learn more. ## Learn more# * * * You can learn more about the architecture that React Aria is a part of on the architecture page. In addition, the following talk from React Europe discusses how React Aria came to be, its architecture, high quality cross-device interactions, and how we can share behavior between design systems. --- ## Page: https://react-spectrum.adobe.com/react-aria/components.html # React Aria Components ## Buttons# * * * Button A button allows a user to perform an action with a mouse, touch, or keyboard. ToggleButton A toggle button allows a user to toggle between two states. ToggleButtonGroup A toggle button group allows a user to toggle multiple options, with single or multiple selection. FileTrigger A file trigger allows a user to access the file system with a Button. ## Pickers# * * * ComboBox A combobox combines a text input with a listbox, and allows a user to filter a list of options. Select A select displays a collapsible list of options, and allows a user to select one of them. ## Collections# * * * Menu A menu displays a list of actions or options that a user can choose. ListBox A listbox displays a list of options, and allows a user to select one or more of them. GridList A grid list displays a list of interactive items, with keyboard navigation, row selection, and actions. Table A table displays data in rows and columns, with row selection and sorting. Tree A tree displays heirarchical data with selection and collapsing. TagGroup A tag group displays a list of items, with support for keyboard navigation, selection, and removal. ## Date and time# * * * DatePicker A date picker combines a DateField and a Calendar popover. DateRangePicker A date range picker combines two DateFields and a RangeCalendar popover. DateField A date field allows a user to enter and edit date values using a keyboard. TimeField A time field allows a user to enter and edit time values using a keyboard. Calendar A calendar allows a user to select a single date from a date grid. RangeCalendar A range calendar allows a user to select a contiguous range of dates. ## Color# * * * ColorPicker A color picker synchronizes a color value between multiple React Aria color components. ColorArea A color area allows users to adjust two channels of a color value. ColorSlider A color slider allows users to adjust an individual channel of a color value. ColorWheel A color wheel allows users to adjust the hue of a color value on a circular track. ColorField A color field allows users to edit a hex color or individual color channel value. ColorSwatch A color swatch displays a preview of a selected color. ColorSwatchPicker A color swatch picker displays a list of color swatches and allows a user to select one of them. ## Overlays# * * * Dialog A dialog is an overlay shown above other content in an application. Popover A popover displays interactive content in context with a trigger element. Tooltip A tooltip displays a description of an element on hover or focus. ## Forms# * * * Checkbox A checkbox allows a user to select an individual option. CheckboxGroup A checkbox group allows a user to select one or more items in a list of options. RadioGroup A radio group allows a user to select a single item from a list of options. Switch A switch allows a user to turn a setting on or off. TextField A text field allows a user to enter a plain text value with a keyboard. SearchField A search field allows a user to enter and clear a search query. NumberField A number field allows a user to enter, increment, or decrement a numeric value. Slider A slider allows a user to select one or more values within a range. Form A form allows users to submit data to a server, with support for validation. ## Navigation# * * * Tabs Tabs organize content into multiple sections, and allow a user to view one at a time. Link A link allows a user to navigate to another page. Breadcrumbs Breadcrumbs display a hierarchy of links to the current page or resource. Disclosure A disclosure is a collapsible section of content. DisclosureGroup A disclosure group is a grouping of related disclosures, sometimes called an accordion. ## Status# * * * ProgressBar A progress bar shows progress of an operation over time. Meter A meter represents a quantity within a known range, or a fractional value. Toast A toast displays a temporary notification of an action, error, or other event. ## Drag and drop# * * * DropZone A drop zone is an area into which one or multiple objects can be dragged and dropped. ## Interactions# * * * usePress Handles press interactions across mouse, touch, keyboard, and screen reader input. useLongPress Handles long press interactions for mouse and touch devices. useHover Handles mouse hover interactions, ignoring touch emulation. useMove Handles move interactions, including mouse and touch drag gestures, and arrow key equivalents. useKeyboard Handles keyboard interactions for a focusable element. useFocus Handles focus interactions for an element, ignoring its descendants. useFocusWithin Handles focus interactions for an element and its descendants. useFocusRing A focus ring is an indication of the active element when interacting with a keyboard. FocusScope A focus scope contains, restores, and manages focus for its descendant elements. --- ## Page: https://react-spectrum.adobe.com/react-aria/examples/index.html # Examples Techniques for styling and animating React Aria Components, and integrating with other libraries. Account Menu A Menu with an interactive header, built with a Dialog and Popover. Action Menu An animated menu of actions, styled with Tailwind CSS. Category Tabs An article category tabs component styled with Tailwind CSS. Command Palette A command palette with actions, styled with Tailwind CSS. Contact List A ListBox featuring sticky section headers and multiple selection. DatePicker A DatePicker component styled with Tailwind CSS. Destructive Alert Dialog An animated confirmation dialog, styled with Tailwind CSS. File System Tree A tree with multiple selection and nested items. Gesture Driven Modal Sheet An iOS-style gesture driven modal sheet built with Framer Motion. Image Grid An async image gallery with selectable items, styled with Tailwind CSS. iOS List View A GridList with swipe gestures, layout animations, and multi selection. Loading ProgressBar A loading ProgressBar styled with Tailwind CSS. Notifications Popover A notifications popover styled with Tailwind CSS. Opacity Slider An opacity slider styled with Tailwind CSS. Ripple Button A button with an animated ripple effect styled with Tailwind CSS. Searchable Select A Select component with Autocomplete filtering. Shipping Radio Group A shipping options RadioGroup styled with Tailwind CSS. Status Select An issue status dropdown styled with Tailwind CSS. Stock Table A table with sticky headers, sorting, multiple selection, and column resizing. Swipeable Tabs A swipeable Tabs component built with Framer Motion and CSS scroll snapping. User Search ComboBox A user search ComboBox styled with Tailwind CSS. Wi-Fi Switch An animated Wi-Fi Switch styled with Tailwind CSS. --- ## Page: https://react-spectrum.adobe.com/react-aria/accessibility.html # Accessibility Building inclusive applications that are accessible to everyone is very important. React Aria helps you build accessible components by providing many aspects of accessibility out of the box, including full screen reader and keyboard navigation support. ## Introduction# * * * Accessibility is the ability for applications to be used by everyone, including those with disabilities. This encompasses all types of disabilities, including vision, auditory, motor, and cognitive disabilities. React Aria addresses aspects of vision and motor disabilities through screen reader and keyboard navigation support. Since it does not provide any rendering or styling, additional consideration should be made in your design process for other types of disabilities. See the inclusive design page on the Spectrum website for more details. Accessibility features also benefit users without disabilities. For example, power users may find it faster to interact with your application using a keyboard, rather than a mouse or touch screen. Especially for applications involving a large amount of data entry, good keyboard navigation support can dramatically increase user productivity. ## ARIA# * * * React Aria implements accessibility support according to the WAI-ARIA specification, published by the W3C. ARIA specifies **semantics** for many common UI controls so that assistive technology like screen readers can understand what our DOM nodes represent. When we cannot use native HTML elements with built-in semantics (e.g. `<button>` or `<select>`) for styling reasons, or if there isn't a native element available, ARIA is required to make presentational elements (e.g. `<div>`) have semantic meaning. This allows screen readers and other assistive technology to understand these elements and announce them properly to the user. However, ARIA only specifies semantics, and it's up to the developer to implement the **behavior** and interactions for each control with JavaScript. The W3C also publishes the ARIA Authoring Practices Guide, which provides patterns and examples of implementing this behavior on top of ARIA. It specifies keyboard interactions that are expected by users of these controls, along with the required roles and states to make them accessible to assistive technology. React Aria provides implementations of ARIA patterns as unstyled React components and hooks, which include both the semantics and behavior out of the box. This makes building accessible components with custom styling much easier, because you only need to implement your custom styles and get the accessibility and behavior for free. ## Labeling# * * * React Aria includes most component semantics by default, but there is one important thing you must provide in your application: a textual label for each control. This gives a screen reader user context about the control they are interacting with. Form inputs like text fields, checkboxes, and selects should usually have a visible label, and in this case, React Aria will automatically associate the visible label with the control so that assistive technology can describe it properly. In case a visible label is not desired for some reason, or you're using a control that doesn't have a built-in label, you must use the `aria-label` or `aria-labelledby` props to identify it to assistive technology. Most React Aria components will display a console warning if you are missing both a visible label and an ARIA label. ## Keyboard navigation# * * * React Aria implements keyboard support for all components, which allows users who cannot use a mouse or touch screen to navigate your app. It also allows power users to navigate your application more quickly, without lifting their hands from the keyboard. All keyboard behavior is implemented according to the W3C's ARIA Authoring Practices Guide, and is designed to feel familiar to users of most commonly used desktop operating systems. For more information about keyboard navigation and focus interactions, see the interactions overview. ## Mobile# * * * On mobile touch screen devices, screen reader users navigate applications by moving a virtual cursor around the screen with gestures like swiping left and right rather than using a keyboard like on desktop platforms. The screen reader knows which elements are available to navigate to through the semantic information exposed by ARIA. These gestures and the virtual cursor take over the whole screen, and normal touch interactions for your application will not be available. There are additional things to consider when supporting mobile screen readers. Since there's no hardware keyboard on most mobile devices, we cannot rely on keyboard interactions being available. And since the screen reader gestures take over the entire screen, we cannot rely on touch interactions. This means that all functionality must be accessible to a screen reader, including things that would typically be handled by keyboard interactions. For example, to close a dialog or popover, mouse and touchscreen users typically click or tap outside. Keyboard users can press the Escape key. On desktop, this may be enough for screen reader users, but on mobile there is no keyboard available. So, we must ensure there is a way for screen reader users to access this functionality without a keyboard. React Aria allows you to handle this by placing a hidden button inside the dialog or popover that screen reader users can navigate to in order to close it. ## Testing# * * * All React Aria components are tested across a wide variety of devices, browsers, and screen readers. * VoiceOver on macOS in Safari and Chrome * JAWS on Windows in Firefox and Chrome * NVDA on Windows in Firefox and Chrome * VoiceOver on iOS * TalkBack on Android in Chrome ### False positives# There are a number of known accessibility false positives in React Aria and React Spectrum, currently documented here in our wiki. These are commonly caught by automated accessibility testing tools and can cause unnecessary noise in your own accessibility audits. To facilitate the suppression of these false positives, the data attribute `data-a11y-ignore` is included on the problematic elements with the relevant `AXE` rule set as its value. Below is a list of the currently available data selectors and their equivalent `AXE` rules: { rules: [ { id: 'aria-hidden-focus', selector: 'body *:not([data-a11y-ignore="aria-hidden-focus"])' } ] } { rules: [ { id: 'aria-hidden-focus', selector: 'body *:not([data-a11y-ignore="aria-hidden-focus"])' } ]; } { rules: [ { id: 'aria-hidden-focus', selector: 'body *:not([data-a11y-ignore="aria-hidden-focus"])' } ]; } This set of rules should be included in your accessibility test framework's `AXE` config, such as in the automated Storybook test runner or for the Storybook a11y addon. --- ## Page: https://react-spectrum.adobe.com/react-aria/interactions.html # Interactions High quality interactions that work across a wide variety of devices are essential to a great user experience. React Aria implements hooks for cross-device interactions that ensure consistent behavior across browsers and platforms. ## Introduction# * * * Building high quality interactions that work across a wide variety of devices and platforms is very difficult today. The web runs across an extremely wide variety of devices and platforms: desktop, mobile, TVs, cars, even refrigerators. This extends to interaction models too: the web supports mouse, touch, keyboard, gamepads, screen readers, and more. Unfortunately, the web platform doesn't have any high level abstractions across these interaction models. There's no high level gesture events, or even the concept of a “press” event that works across all of these interaction models. We just have low level events like mouse, touch, keyboard, and focus events, and it's up to developers to put them together properly. This leads to many web apps not working consistently across various types of devices and interaction models. React Aria includes a collection of Hooks that provide higher level abstractions over the low level events exposed by the web platform, and helps normalize the behavior across browsers and devices. This includes support for high level events like “press”, “hover”, and “focus”. Some of these seem simple at first, but you’d be surprised how complicated it is to handle these events in a cross-browser, cross-device way. There are many tiny behavioral differences that have a big impact on how well components work across all of these platforms. ## Pointer events# * * * The web was originally designed for mouse events. Later, when touch devices were introduced, browsers added support for touch events. However, since existing web apps had not been designed with touch in mind, browsers needed to emulate mouse events on touch devices to ensure some compatibility with them. Touch input is quite different than mouse input though. For example, it is possible to hover over an element with a mouse, but not with touch. But since existing apps designed for mouse input have hover only effects, browsers emulate hover events on touch devices. Touching an element once fires `onMouseEnter` and touching it again fires `onMouseLeave`. On one hand this is good because it means the functionality is accessible to users on touch devices, but on the other hand, it's not a great experience. Mobile browsers also support many types of gestures by default, e.g. double tap to zoom, scrolling, and long press to select text. Because of this, touch devices often delay firing events like `onClick` in order to first determine if the user intends to zoom or scroll the page. In the context of a highly interactive web application, this leads to a sub-optimal user experience. The new pointer events spec helps simplify handling events across mouse, touch, and stylus/pen interaction models. React Aria uses pointer events where available with fallbacks on older devices to mouse and touch events. It normalizes many cross-browser behavioral differences, and ensures that all browsers and platforms behave consistently. In addition, React Aria supports additional interaction models like keyboard and focus events to ensure that all users can interact with your app, not just mouse and touchscreen users. CSS pseudo classes like `:hover` and `:active` are also problematic in many cases, especially on hybrid devices that support both mouse and touch. They also emulate mouse behavior on touch devices, e.g. showing hover effects when you touch elements. It's best to use JavaScript events that apply CSS styles rather than these pseudo classes for the best cross-device experience. See usePress and useHover for more details about React Aria's hooks for cross device interactions. ## Keyboard and focus behavior# * * * Keyboard and focus support is important to allow users to navigate your app with a keyboard. This is imperative for users who are unable to physically use a mouse or touch screen, but also nice for power users who may find it faster to navigate parts of your app without lifting their hands from the keyboard. At a high level, keyboard navigation is broken up into **tab stops**, which may be navigated by pressing the Tab key to move to the next tab stop, and Shift + Tab to move to the previous tab stop. A tab stop may be an atomic component like a text field or button, or a composite component like a listbox, radio group, grid, or toolbar. Composite components behave as a single tab stop. Elements within a composite component are typically navigated with the arrow keys, while the Tab key continues to navigate to the next/previous tab stop. React Aria implements many of these composite components and handles all of the keyboard navigation behavior for elements inside them. Overlay elements like dialogs, popovers, and menus have additional focus behavior to ensure that focus stays within them while they are open, and focus is restored back to the element that opened them when they are closed. In React Aria this is implemented by the FocusScope component. Another important feature for keyboard users is a **focus ring**. This is a visual affordance for the currently focused element, which allows a keyboard user to know which element they are currently on. It should only be visible when navigating with a keyboard, so as not to distract mouse and touchscreen users. This can be implemented using the useFocusRing hook or the FocusRing component. The useFocusVisible hook can also be used to determine whether the user is currently navigating with a keyboard or not. ## Assistive technology# * * * An assistive technology, such as a screen reader, relies on semantic information exposed to the browser through ARIA or native HTML element semantics. This allows it to know what an element represents and relay this information to the user. As a screen reader or other assistive technology navigates around an application, it may fire various JavaScript events. Since screen readers are navigating the app in a completely different way than traditional mouse, touch, or keyboard behavior, these events are emulated by a screen reader. It may focus elements, fire click events to activate them, or otherwise emulate mouse and keyboard events. React Aria is careful to handle events fired by assistive technology, and normalizes this behavior as needed so it is consistent with other interactions. For example, while we use pointer events for mouse and touch interactions, many screen readers do not fire these events at all, instead only firing `onClick`. React Aria handles this and simulates press start and press end events so that components receive a consistent stream of events regardless of the interaction model. ## Supported browsers and platforms# * * * All React Aria components are tested across a wide variety of browsers and devices. We test across devices with mouse input, touchscreens, and also hybrid devices. * Chrome on macOS, Windows, and Android * Firefox on macOS and Windows * Safari on macOS, iOS, and iPadOS * Edge on Windows --- ## Page: https://react-spectrum.adobe.com/react-aria/internationalization.html # Internationalization Adapting components to respect languages and cultures of users around the world is an important way to help make your application accessible to the widest number of people. React Aria supports over 30 languages, including right-to-left mirroring and interactions. ## Introduction# * * * Internationalization is the process of structuring your code and user interface to support localization. React Aria supports many aspects of localization for many components out of the box, including translations for builtin strings, localized date and number formatting, right-to-left interactions, and more. By using React Aria to build your components, these aspects of internationalization are handled for you. You can also use our hooks in your own custom components. ## Localization# * * * Localization is the process of adapting an application for a particular language or region. It includes translating text content, as well as adapting date formatting, number formatting, collation and sorting, text search, and more. React Aria includes translations for all builtin strings in over 30 locales. However, because React Aria provides no rendering, most of the builtin strings are for non-visible content to provide accessible labels. All application provided content must be localized and passed in to components. This can be done with libraries such as react-intl. Internally, React Aria uses the same intl-messageformat library used by react-intl. React Aria also formats, parses, and manipulates dates and numbers according to the user's preferred locale, and uses internationalized algorithms for collation, sorting, and text search. This is built into all of the components that need to perform these tasks out of the box, but you can also use our internationalization hooks in your own components. These are implemented with the builtin browser Intl APIs under the hood, so there's no large libraries or locale data for users to download. See useDateFormatter, useNumberFormatter, and useCollator for more information about using our internationalization hooks. The Internationalized collection of libraries provides framework-agnostic utilities for representing and manipulating dates and times, and parsing and formatting numbers across many locales, calendar systems, numbering systems, and more. ## Bi-directionality# * * * Many languages such as those written in the Latin script (e.g. English and French), Cyrillic script (e.g. Russian and Bulgarian), and logographic scripts (e.g. Chinese and Japanese) are written left to right. Other languages such as Arabic and Hebrew are written right to left. These languages are called “bi-directional,” or are also commonly referred to as “RTL” (“right-to-left”) languages. In right-to-left languages, user interfaces are expected to be **mirrored**. Layouts flip such that components are positioned on the opposite side of the interface. For example, a button that would be displayed on the right side of a screen in a left-to-right language would appear on the left side of the screen in a right-to-left language. In addition, elements within a component are also expected to mirror. For example, in a checkbox component, the label would be placed on the left side of the checkmark rather than the right. Since React Aria provides no styling, it is up to you to implement RTL support in your design. CSS flex and grid layouts automatically flip depending on the direction, and logical properties are a great way to handle margins, paddings, borders, and more. Even though it is not responsible for rendering or layout, React Aria is aware of the current directionality, and adjusts interactions accordingly. For example, when navigating using the left and right arrow keys, React Aria automatically flips the direction so that the left arrow always navigates to the item physically to the left, and the right arrow always navigates to the item physically to the right. ## Example# * * * The root most element of your application should define the lang and dir attributes so that the browser knows which language and direction the user interface should be rendered in. This can be done with the useLocale hook. import {useLocale} from 'react-aria-components'; function YourApp() { let {locale, direction} = useLocale(); return ( <div lang={locale} dir={direction}> {/* your app here */} </div> ); } import {useLocale} from 'react-aria-components'; function YourApp() { let {locale, direction} = useLocale(); return ( <div lang={locale} dir={direction}> {/* your app here */} </div> ); } import {useLocale} from 'react-aria-components'; function YourApp() { let { locale, direction } = useLocale(); return ( <div lang={locale} dir={direction} > {/* your app here */} </div> ); } React Aria automatically detects the user's current language by default, and even updates this if the browser or system language changes. However, if you would like to override this with an application specific setting, you can do so using the I18nProvider component. import {I18nProvider} from 'react-aria-components'; <I18nProvider locale="fr-FR"> <YourApp /> </I18nProvider> import {I18nProvider} from 'react-aria-components'; <I18nProvider locale="fr-FR"> <YourApp /> </I18nProvider> import {I18nProvider} from 'react-aria-components'; <I18nProvider locale="fr-FR"> <YourApp /> </I18nProvider> **Note:** if you are using server side rendering, you should always specify a locale rather than relying on browser defaults to ensure that the server and client match. See the SSR docs for more information. ## Supported locales# * * * React Aria currently includes translations for over 30 locales. They are listed below. * Arabic (United Arab Emirates) * Bulgarian (Bulgaria) * Chinese (Simplified) * Chinese (Traditional) * Croatian (Croatia) * Czech (Czech Republic) * Danish (Denmark) * Dutch (Netherlands) * English (Great Britain) * English (United States) * Estonian (Estonia) * Finnish (Finland) * French (Canada) * French (France) * German (Germany) * Greek (Greece) * Hebrew (Israel) * Hungarian (Hungary) * Italian (Italy) * Japanese (Japan) * Korean (Korea) * Latvian (Latvia) * Lithuanian (Lithuania) * Norwegian (Norway) * Polish (Poland) * Portuguese (Brazil) * Romanian (Romania) * Russian (Russia) * Serbian (Serbia) * Slovakian (Slovakia) * Slovenian (Slovenia) * Spanish (Spain) * Swedish (Sweden) * Turkish (Turkey) * Ukrainian (Ukraine) ## Optimizing bundle size# * * * By default, React Aria includes translations for all of the languages listed above. This is inclusive to the most users out of the box, but comes at the cost of bundle size. If your application does not support all of these locales, you can use our build plugins to include only the languages that you do support in your JavaScript bundle. **Note**: If you are using server side rendering, you can also optimize this even further by sending only the strings for the current user's language. See the SSR docs for more details. ### webpack# First, install `@react-aria/optimize-locales-plugin` with your package manager. Then, add the following to your `webpack.config.js` and change the `locales` setting accordingly: // webpack.config.js const optimizeLocales = require('@react-aria/optimize-locales-plugin'); module.exports = { // ... plugins: [ optimizeLocales.webpack({ locales: ['en-US', 'fr-FR'] }) ] }; // webpack.config.js const optimizeLocales = require( '@react-aria/optimize-locales-plugin' ); module.exports = { // ... plugins: [ optimizeLocales.webpack({ locales: ['en-US', 'fr-FR'] }) ] }; // webpack.config.js const optimizeLocales = require( '@react-aria/optimize-locales-plugin' ); module.exports = { // ... plugins: [ optimizeLocales .webpack({ locales: [ 'en-US', 'fr-FR' ] }) ] }; ### Next.js# First, install `@react-aria/optimize-locales-plugin` with your package manager. Then, add the following to your `next.config.js` and change the `locales` setting accordingly: // next.config.js const optimizeLocales = require('@react-aria/optimize-locales-plugin'); module.exports = { webpack(config) { config.plugins.push( optimizeLocales.webpack({ locales: ['en-US', 'fr-FR'] }) ); return config; } }; // next.config.js const optimizeLocales = require( '@react-aria/optimize-locales-plugin' ); module.exports = { webpack(config) { config.plugins.push( optimizeLocales.webpack({ locales: ['en-US', 'fr-FR'] }) ); return config; } }; // next.config.js const optimizeLocales = require( '@react-aria/optimize-locales-plugin' ); module.exports = { webpack(config) { config.plugins.push( optimizeLocales .webpack({ locales: [ 'en-US', 'fr-FR' ] }) ); return config; } }; ### Vite# First, install `@react-aria/optimize-locales-plugin` with your package manager. Then, add the following to your `vite.config.js` and change the `locales` setting accordingly: // vite.config.js import optimizeLocales from '@react-aria/optimize-locales-plugin'; export default { plugins: [ { ...optimizeLocales.vite({ locales: ['en-US', 'fr-FR'] }), enforce: 'pre' } ] }; // vite.config.js import optimizeLocales from '@react-aria/optimize-locales-plugin'; export default { plugins: [ { ...optimizeLocales.vite({ locales: ['en-US', 'fr-FR'] }), enforce: 'pre' } ] }; // vite.config.js import optimizeLocales from '@react-aria/optimize-locales-plugin'; export default { plugins: [ { ...optimizeLocales .vite({ locales: [ 'en-US', 'fr-FR' ] }), enforce: 'pre' } ] }; ### Rollup# First, install `@react-aria/optimize-locales-plugin` with your package manager. Then, add the following to your `rollup.config.js` and change the `locales` setting accordingly: // rollup.config.js import optimizeLocales from '@react-aria/optimize-locales-plugin'; export default { plugins: [ optimizeLocales.rollup({ locales: ['en-US', 'fr-FR'] }) ] }; // rollup.config.js import optimizeLocales from '@react-aria/optimize-locales-plugin'; export default { plugins: [ optimizeLocales.rollup({ locales: ['en-US', 'fr-FR'] }) ] }; // rollup.config.js import optimizeLocales from '@react-aria/optimize-locales-plugin'; export default { plugins: [ optimizeLocales .rollup({ locales: [ 'en-US', 'fr-FR' ] }) ] }; ### Esbuild# First, install `@react-aria/optimize-locales-plugin` with your package manager. Then, add the following to your esbuild script and change the `locales` setting accordingly: import {build} from 'esbuild'; import optimizeLocales from '@react-aria/optimize-locales-plugin'; build({ plugins: [ optimizeLocales.esbuild({ locales: ['en-US', 'fr-FR'] }) ] }); import {build} from 'esbuild'; import optimizeLocales from '@react-aria/optimize-locales-plugin'; build({ plugins: [ optimizeLocales.esbuild({ locales: ['en-US', 'fr-FR'] }) ] }); import {build} from 'esbuild'; import optimizeLocales from '@react-aria/optimize-locales-plugin'; build({ plugins: [ optimizeLocales .esbuild({ locales: [ 'en-US', 'fr-FR' ] }) ] }); ### Parcel# First, install `@react-aria/parcel-resolver-optimize-locales` with your package manager. Then, add the following to your `.parcelrc`: { "extends": "@parcel/config-default", "resolvers": ["@react-aria/parcel-resolver-optimize-locales", "..."] } { "extends": "@parcel/config-default", "resolvers": ["@react-aria/parcel-resolver-optimize-locales", "..."] } { "extends": "@parcel/config-default", "resolvers": ["@react-aria/parcel-resolver-optimize-locales", "..."] } Then, in your project root `package.json`, add a `"locales"` field containing the languages you want to support: { "locales": ["en-US", "fr-FR"] } { "locales": ["en-US", "fr-FR"] } { "locales": ["en-US", "fr-FR"] } --- ## Page: https://react-spectrum.adobe.com/react-aria/testing.html # Testing This page describes how to test an application built with React Aria. It documents the available testing utilities available for each aria pattern and how they can be used to simulate common user interactions. ## Introduction# * * * Running automated tests on your application helps ensure that it continues to work as expected over time. You can use testing tools like React Testing Library along with test runners like Jest or Mocha to test applications built with React Aria Components or hooks. These generally work quite well out of the box but there are a few things to consider to ensure your tests are the best they can be. The information below covers best practices when writing tests, and be sure to checkout our test utils that incorporate these strategies under the hood, helping streamline the test writing practice for you. ## Testing semantics# * * * Many testing libraries expect you to query for elements in the DOM tree. For example, you might have a test that renders a login page, finds the username and password fields, and simulates filling them out and submitting the form. The recommended way to query for React Aria Components and their internals is by semantics. React Aria Components implement ARIA patterns. ARIA is a W3C standard that specifies the semantics for many UI components. This is used to expose them to assistive technology such as screen readers, but can also be used in tests to operate the application programmatically. These semantics are much less likely to change over time, and while other DOM nodes may be added or removed, the semantics are more likely to stay stable. The main attribute to look for when querying is the role. This attribute represents the type of element a DOM node represents, e.g. a button, list option, or tab. ### React Testing Library# React Testing Library is useful because it enforces that you write tests using semantics instead of implementation details. We use React Testing Library to test React Aria itself, and it's quite easy to query elements by role, text, label, etc. import {render} from '@testing-library/react'; let tree = render(<MyComponent />); let option = tree.getByRole('button'); import {render} from '@testing-library/react'; let tree = render(<MyComponent />); let option = tree.getByRole('button'); import {render} from '@testing-library/react'; let tree = render( <MyComponent /> ); let option = tree .getByRole('button'); ## Test ids# * * * Querying by semantics covers many scenarios, but what if you have many buttons on a page? How do you find the specific button you're looking for in a test? In many cases this could be done by querying by the text in the button or an accessibility label associated with it, but sometimes this might change over time or may be affected by things like translations in different languages. In these cases, you may need a way to identify specific elements in tests, and that's where test ids come in. React Aria Components pass all data attributes through to their underlying DOM nodes, which allows you to use an attribute like `data-testid` to identify a particular instance of a component. For example, you could add test ids to the two input elements in a login form and use them to find the username and password fields. This example uses React Testing Library, but the idea could be applied in a similar way with other testing libraries. import {render} from '@testing-library/react'; import {Input, Label, TextField} from 'react-aria-components'; function LoginForm() { return ( <> <TextField data-testid="username"> <Label>Username</Label> <Input /> </TextField> <TextField data-testid="password"> <Label>Username</Label> <Input /> </TextField> </> ); } let tree = render(<LoginForm />); let username = tree.getByTestId('username'); let password = tree.getByTestId('password'); import {render} from '@testing-library/react'; import { Input, Label, TextField } from 'react-aria-components'; function LoginForm() { return ( <> <TextField data-testid="username"> <Label>Username</Label> <Input /> </TextField> <TextField data-testid="password"> <Label>Username</Label> <Input /> </TextField> </> ); } let tree = render(<LoginForm />); let username = tree.getByTestId('username'); let password = tree.getByTestId('password'); import {render} from '@testing-library/react'; import { Input, Label, TextField } from 'react-aria-components'; function LoginForm() { return ( <> <TextField data-testid="username"> <Label> Username </Label> <Input /> </TextField> <TextField data-testid="password"> <Label> Username </Label> <Input /> </TextField> </> ); } let tree = render( <LoginForm /> ); let username = tree .getByTestId( 'username' ); let password = tree .getByTestId( 'password' ); ## Triggering events# * * * Most testing libraries include a way to simulate events on an element. React Aria Components rely on many different browser events to support different devices and platforms, so it's important to simulate these correctly in your tests. For example, rather than only simulating a click event, the tests should simulate all of the events that would occur if a real user were interacting with the component. For example, a click is really a `mousemove` and `mouseover` the target, followed by `mousedown`, `focus`, and `mouseup` events, and finally a `click` event. If you only simulated the `click` event, you would be missing all of these other preceding events that occur in real-world situations and this may make your test not work correctly. The implementation of the component may also change in the future to expect these events, making your test brittle. In addition, browsers have default behavior that occurs on certain events which would be missing, like focusing elements on mouse down, and toggling checkboxes on click. The best way to handle this is with the user-event library. This lets you trigger high level interactions like a user would, and the library handles firing all of the individual events that make up that interaction. If you use this library rather than firing events manually, your tests will simulate real-world behavior much better and work as expected. user-event can handle many types of interactions, e.g. clicks, tabbing, typing, etc. This example shows how you could use it to render a login form and enter text in each field and click the submit button, just as a real user would. import {render} from '@testing-library/react'; import userEvent from '@testing-library/user-event'; let tree = render(<LoginForm />); // Click on the username field to focus it, and enter the value. userEvent.click(tree.getByLabelText('Username')); userEvent.type(document.activeElement, 'devon'); // Tab to the password field, and enter the value. userEvent.tab(); userEvent.type(document.activeElement, 'Pas$w0rd'); // Tab to the submit button and click it. userEvent.tab(); userEvent.click(document.activeElement); import {render} from '@testing-library/react'; import userEvent from '@testing-library/user-event'; let tree = render(<LoginForm />); // Click on the username field to focus it, and enter the value. userEvent.click(tree.getByLabelText('Username')); userEvent.type(document.activeElement, 'devon'); // Tab to the password field, and enter the value. userEvent.tab(); userEvent.type(document.activeElement, 'Pas$w0rd'); // Tab to the submit button and click it. userEvent.tab(); userEvent.click(document.activeElement); import {render} from '@testing-library/react'; import userEvent from '@testing-library/user-event'; let tree = render( <LoginForm /> ); // Click on the username field to focus it, and enter the value. userEvent.click( tree.getByLabelText( 'Username' ) ); userEvent.type( document.activeElement, 'devon' ); // Tab to the password field, and enter the value. userEvent.tab(); userEvent.type( document.activeElement, 'Pas$w0rd' ); // Tab to the submit button and click it. userEvent.tab(); userEvent.click( document.activeElement ); ## React Aria test utils alpha# * * * @react-aria/test-utils is a set of testing utilities that aims to make writing unit tests easier for consumers of React Aria or for users who have built their own components following the respective ARIA pattern specification. By using the ARIA specification for any given component pattern as a source of truth, we can make assumptions about the existence of various aria attributes in a component. This allows us to navigate the component's DOM structure, simulate common interactions, and verify the the resulting state of the component. ### Installation# `@react-aria/test-utils` can be installed using a package manager like npm or yarn. yarn add --dev @react-aria/test-utils Please note that this library uses @testing-library/react@16 and @testing-library/user-event@14. This means that you need to be on React 18+ in order for these utilities to work. ### Setup# Once installed, you can access the `User` that `@react-aria/test-utils` provides in your test file as shown below. This user only needs to be initialized once and then can be used to generate specific ARIA pattern tester in your test cases. This gives you access to that pattern's specific utilities that you can then call within your test to query for specific subcomponents or simulate common interactions. See below for what patterns are currently supported. // YourTest.test.ts import {screen} from '@testing-library/react'; import {User} from '@react-aria/test-utils'; // Provide whatever method of advancing timers you use in your test, this example assumes Jest with fake timers. // 'interactionType' specifies what mode of interaction should be simulated by the tester // 'advanceTimer' is used by the tester to advance the timers in the tests for specific interactions (e.g. long press) let testUtilUser = new User({ interactionType: 'mouse', advanceTimer: jest.advanceTimersByTime }); // ... it('my test case', async function () { // Render your test component/app render(); // Initialize the table tester via providing the 'Table' pattern name and the root element of said table let table = testUtilUser.createTester('Table', { root: screen.getByTestId('test_table') }); // ... }); // YourTest.test.ts import {screen} from '@testing-library/react'; import {User} from '@react-aria/test-utils'; // Provide whatever method of advancing timers you use in your test, this example assumes Jest with fake timers. // 'interactionType' specifies what mode of interaction should be simulated by the tester // 'advanceTimer' is used by the tester to advance the timers in the tests for specific interactions (e.g. long press) let testUtilUser = new User({ interactionType: 'mouse', advanceTimer: jest.advanceTimersByTime }); // ... it('my test case', async function () { // Render your test component/app render(); // Initialize the table tester via providing the 'Table' pattern name and the root element of said table let table = testUtilUser.createTester('Table', { root: screen.getByTestId('test_table') }); // ... }); // YourTest.test.ts import {screen} from '@testing-library/react'; import {User} from '@react-aria/test-utils'; // Provide whatever method of advancing timers you use in your test, this example assumes Jest with fake timers. // 'interactionType' specifies what mode of interaction should be simulated by the tester // 'advanceTimer' is used by the tester to advance the timers in the tests for specific interactions (e.g. long press) let testUtilUser = new User({ interactionType: 'mouse', advanceTimer: jest .advanceTimersByTime }); // ... it('my test case', async function () { // Render your test component/app render(); // Initialize the table tester via providing the 'Table' pattern name and the root element of said table let table = testUtilUser .createTester( 'Table', { root: screen .getByTestId( 'test_table' ) } ); // ... }); See below for the full definition of the `User` object. ### Properties | Name | Type | Default | Description | | --- | --- | --- | --- | | `interactionType` | ` UserOpts ['interactionType']` | `mouse` | The interaction type (mouse, touch, keyboard) that the test util user will use when interacting with a component. This can be overridden at the aria pattern util level if needed. | | `advanceTimer` | ` UserOpts ['advanceTimer']` | — | A function used by the test utils to advance timers during interactions. Required for certain aria patterns (e.g. table). | ### Methods | Method | Description | | --- | --- | | `constructor( (opts: UserOpts )): void` | | | `createTester<T extends PatternNames >( (patternName: T, , opts: TesterOpts <T> )): Tester <T>` | Creates an aria pattern tester, inheriting the options provided to the original user. | ### Patterns# Below is a list of the ARIA patterns testers currently supported by `createTester`. See the accompanying component testing docs pages for a sample of how to use the testers in your test suite. * ComboBox * GridList * ListBox * Menu * Select * Table * Tabs * Tree --- ## Page: https://react-spectrum.adobe.com/react-aria/dnd.html # Drag and Drop Drag and drop is an intuitive way for users to transfer data between locations. React Aria implements drag and drop for mouse and touch interactions, and provides full keyboard and screen reader accessibility. ## Introduction# * * * Drag and drop is a common UI interaction that allows users to transfer data between two locations by directly moving a visual representation on screen. It is a flexible, efficient, and intuitive way for users to perform a variety of tasks, and is widely supported across both desktop and mobile operating systems. In addition to the standard mouse and touch interactions, React Aria also implements keyboard and screen reader accessible alternatives for drag and drop to enable all users to perform these tasks. ## Concepts# * * * Drag and drop allows a user to move data between two locations. The initial location is referred to as a **drag source**, and the final location is referred to as a **drop target**. The source and target may be within the same application, for example two adjacent lists, or in different applications, for example uploading files from the desktop. The drag source is responsible for providing one or more **drag items**, which specify the data to be dragged. A drag item may be an object represented by the draggable element, a file, plain text content, etc. Each drag item includes a type, and the actual data for the item. The type of a drag item can be one of the common mime types or a custom type specific to the application. Multiple representations of the same object may be provided in different formats for interoperability with various drop targets. This allows drag and drop to work even between different applications. For example, a drag source may provide the data for a drag in both a custom object format and as plain text. This allows dropping within the application to include richer functionality, but also allows the user to drop in external applications such as email clients or text editors. While dragging, a **drag preview** is displayed under the user's mouse or finger to indicate the items being dragged. By default, this is a copy of the dragged element, but it can be customized. For example, when multiple items are dragged, they could be shown as a stack, or with a badge indicating the count. There are several **drop operations** that can be performed as a result of a drag and drop interaction: * `move` – the dragged data is moved from its source location to the target location. * `copy` – the dragged data is copied to the target destination. * `link` – a relationship is established between the source and target locations. * `cancel` – the drag and drop operation is canceled, resulting in no changes made to the source or target. Many operating systems display drop operations in the form of a cursor change when hovering over a drop target, e.g. a plus sign to indicate a copy operation. The user may also be able to use a modifier key to choose which drop operation to perform, such as Option or Alt to switch from move to copy. The drag source can also specify what drop operations are allowed for its data, allowing the drop target to decide what operation to perform, using the restrictions set by the drag source as a guideline. Visual feedback for a copy drop operation. Collection components such as ListBox, Table, and GridList support multiple **drop positions**. The collection may support a `"root"` drop position, allowing items to be dropped on the collection as a whole. It may also support `"on"` drop positions, such as when dropping into a folder in a list. If the collection allows the user to control the order of the items, it may also support `"between"` drop positions, allowing the user to insert or move items between other items. This is indicated by rendering a **drop indicator** between two items when the user drags over the space between them. Any number of these drop positions can be allowed at the same time and the component can use the types of the dragged items to selectively allow or disallow certain positions. The "root", "on", and "between" drop positions. ## Interactions# * * * While drag and drop has historically been mostly limited to mouse and touchscreen users, keyboard and screen reader friendly alternatives are important for users who cannot use these interaction methods. For example, copy and paste can often be used as an alternative to drag and drop to allow the user to move an object from one location to another. However, copy and paste does not cover all of the possible interactions that drag and drop allows. For example, it is hard to specify an exact location to insert an item into a list on paste, or even know where pasting is accepted. Users must either already know where they can paste, or navigate aimlessly until they find a valid location. React Aria implements keyboard and screen reader friendly interactions for drag and drop that provide full parity with the mouse and touch experiences. Users can press Enter on a draggable element to enter drag and drop mode. Then, they can press Tab to cycle between the drop targets that accept the dragged data, then press Enter to drop. Escape can be pressed at any time to cancel. Touch screen reader users can also drag by double tapping to activate drag and drop mode, swiping between drop targets, and double tapping again to drop. Screen reader announcements are included to help guide the user through this process. All of this is built into the useDrag and useDrop hooks, and components such as DropZone. Collection components such as lists or tables are treated as a single drop target, so that users can easily tab past them to get to the next drop target without going through every item. Instead, within a droppable collection, keys such as ArrowDown and ArrowUp can be used to select a drop position, such as on an item, or between items. The exact interactions may vary depending on the layout of the collection component. Drag and drop for collections is implemented by the useDraggableCollection and useDroppableCollection hooks, which are built into in the ListBox, Table, and GridList components. Draggable elements can sometimes have other interactions that conflict with the keyboard and screen reader interactions needed to initiate a drag, such as the Enter key. In these cases, an explicit **drag affordance** may be added. Keyboard and screen reader users can focus this element, and use it to initiate drag and drop for the parent item. In addition, this has the added benefit of making drag and drop more discoverable. A focusable drag affordance to initiate keyboard and screen reader drag and drop. Note that because mouse and touch drag and drop interactions utilize the native browser APIs, they work both within the browser window and with external applications on the user's device. Keyboard and screen reader drag and drop is implemented from scratch, and therefore can only be supported within the browser window. Alternative interactions for operations involving external applications, such as file uploading, should be implemented in addition to drag and drop to ensure that all users can perform these tasks. One possible way to do this is via copy and paste. See the useClipboard hook for more details. --- ## Page: https://react-spectrum.adobe.com/react-aria/styling.html # 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> Render props also let you modify the default values provided by React Aria via the `defaultClassName`, `defaultStyle`, and `defaultChildren` options. For example, you could wrap the default children of a `SelectValue` in an extra element, append an additional class name to React Aria's default, or merge default inline styles with your own. <SelectValue> {({defaultChildren}) => <span>{defaultChildren}</span>} </SelectValue> <SelectValue> {({defaultChildren}) => <span>{defaultChildren}</span>} </SelectValue> <SelectValue> {( { defaultChildren } ) => ( <span> {defaultChildren} </span> )} </SelectValue> 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 When using Tailwind v4, add it to your CSS: @import "tailwindcss"; @plugin "tailwindcss-react-aria-components"; @import "tailwindcss"; @plugin "tailwindcss-react-aria-components"; @import "tailwindcss"; @plugin "tailwindcss-react-aria-components"; Tailwind v3 When using Tailwind v3, install `tailwindcss-react-aria-components` version 1.x instead of 2.x, and add the plugin to your `tailwind.config.js` instead: /** @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. @plugin "tailwindcss-react-aria-components" { prefix: rac }; @plugin "tailwindcss-react-aria-components" { prefix: rac }; @plugin "tailwindcss-react-aria-components" { prefix: rac }; Tailwind v3 When using Tailwind v3, pass the prefix option to the plugin in `tailwind.config.js`: /** @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 supports both CSS transitions and keyframe animations, and works with JavaScript animation libraries like Framer Motion. ### CSS transitions# 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. * `[data-entering]` represents the starting state of the entry animation. The component will transition from the entering state to the default state when it opens. * `[data-exiting]` represents the ending state of the exit animation. The component will transition from the default state to the exiting state and wait for any animations to complete before being removed from the DOM. .react-aria-Popover { transition: opacity 300ms; &[data-entering], &[data-exiting] { opacity: 0; } } .react-aria-Popover { transition: opacity 300ms; &[data-entering], &[data-exiting] { opacity: 0; } } .react-aria-Popover { transition: opacity 300ms; &[data-entering], &[data-exiting] { opacity: 0; } } Note that the `[data-entering]` state is only applied for one frame when using CSS transitions. The transition itself should be assigned in the default state. To create a different exit animation, assign the transition in the `[data-exiting]` state. .react-aria-Popover { /* entry transition */ transition: transform 300ms, opacity 300ms; /* starting state of the entry transition */ &[data-entering] { opacity: 0; transform: scale(0.8); } &[data-exiting] { /* exit transition */ transition: opacity 150ms; /* ending state of the exit transition */ opacity: 0; } } .react-aria-Popover { /* entry transition */ transition: transform 300ms, opacity 300ms; /* starting state of the entry transition */ &[data-entering] { opacity: 0; transform: scale(0.8); } &[data-exiting] { /* exit transition */ transition: opacity 150ms; /* ending state of the exit transition */ opacity: 0; } } .react-aria-Popover { /* entry transition */ transition: transform 300ms, opacity 300ms; /* starting state of the entry transition */ &[data-entering] { opacity: 0; transform: scale(0.8); } &[data-exiting] { /* exit transition */ transition: opacity 150ms; /* ending state of the exit transition */ opacity: 0; } } ### CSS animations# For more complex animations, you can also apply CSS keyframe animations using the same `[data-entering]` and `[data-exiting]` states. .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; } } Note that unlike CSS transitions, keyframe animations are not interruptible. If the user opens and closes an overlay quickly, the animation may appear to jump to the ending state before the next animation starts. ### 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> --- ## Page: https://react-spectrum.adobe.com/react-aria/forms.html # Forms Forms allow users to enter and submit data, and provide them with feedback along the way. React Aria includes many components that integrate with HTML forms, with support for custom validation and styling. ## Labels and help text# * * * Accessible forms start with clear, descriptive labels for each field. All React Aria form components support labeling using the `Label` component, which is automatically associated with the field via the `id` and `for` attributes on your behalf. In addition, React Aria components support help text, which associates additional context with a field such as a description or error message. The label and help text are announced by assistive technology such as screen readers when the user focuses the field. import {TextField, Label, Input, Text} from 'react-aria-components'; <TextField type="password"> <Label>Password</Label> <Input /> <Text slot="description">Password must be at least 8 characters.</Text></TextField> import { Input, Label, Text, TextField } from 'react-aria-components'; <TextField type="password"> <Label>Password</Label> <Input /> <Text slot="description"> Password must be at least 8 characters. </Text></TextField> import { Input, Label, Text, TextField } from 'react-aria-components'; <TextField type="password"> <Label> Password </Label> <Input /> <Text slot="description"> Password must be at least 8 characters. </Text></TextField> PasswordPassword must be at least 8 characters. Most fields should have a visible label. In rare exceptions, the `aria-label` or `aria-labelledby` attribute must be provided instead to identify the element to screen readers. ## Submitting data# * * * How you submit form data depends on your framework, application, and server. By default, HTML forms are submitted by the browser using a full page refresh. You can take control of form submission by calling `preventDefault` during the `onSubmit` event, and make an API call to submit the data however you like. Frameworks like Next.js and Remix also include builtin APIs to manage this for you. ### Uncontrolled forms# The simplest way to get data from a form is using the browser's FormData API during the `onSubmit` event. This can be passed directly to fetch, or converted into a regular JavaScript object using Object.fromEntries. Each field should have a `name` prop to identify it, and values will be serialized to strings by the browser. import {Button, Form, Input, Label, TextField} from 'react-aria-components'; function Example() { let [submitted, setSubmitted] = React.useState(null); let onSubmit = (e: React.FormEvent<HTMLFormElement>) => { // Prevent default browser page refresh. e.preventDefault(); // Get form data as an object. let data = Object.fromEntries(new FormData(e.currentTarget)); // Submit to your backend API... setSubmitted(data); }; return ( <Form onSubmit={onSubmit}> <TextField name="name"> <Label>Name</Label> <Input /> </TextField> <Button type="submit">Submit</Button> {submitted && ( <div> You submitted: <code>{JSON.stringify(submitted)}</code> </div> )} </Form> ); } import { Button, Form, Input, Label, TextField } from 'react-aria-components'; function Example() { let [submitted, setSubmitted] = React.useState(null); let onSubmit = (e: React.FormEvent<HTMLFormElement>) => { // Prevent default browser page refresh. e.preventDefault(); // Get form data as an object. let data = Object.fromEntries( new FormData(e.currentTarget) ); // Submit to your backend API... setSubmitted(data); }; return ( <Form onSubmit={onSubmit}> <TextField name="name"> <Label>Name</Label> <Input /> </TextField> <Button type="submit">Submit</Button> {submitted && ( <div> You submitted:{' '} <code>{JSON.stringify(submitted)}</code> </div> )} </Form> ); } import { Button, Form, Input, Label, TextField } from 'react-aria-components'; function Example() { let [ submitted, setSubmitted ] = React.useState( null ); let onSubmit = ( e: React.FormEvent< HTMLFormElement > ) => { // Prevent default browser page refresh. e.preventDefault(); // Get form data as an object. let data = Object .fromEntries( new FormData( e.currentTarget ) ); // Submit to your backend API... setSubmitted(data); }; return ( <Form onSubmit={onSubmit} > <TextField name="name"> <Label> Name </Label> <Input /> </TextField> <Button type="submit"> Submit </Button> {submitted && ( <div> You submitted: {' '} <code> {JSON .stringify( submitted )} </code> </div> )} </Form> ); } Name Submit ### Controlled forms# By default, all React Aria components are uncontrolled, which means that the state is stored internally on your behalf. If you need access to the value in realtime, as the user is editing, you can make it controlled. You'll need to manage the state using React's useState hook, and pass the current value and a change handler into each form component. import {Form, TextField, Label, Input, Button} from 'react-aria-components'; function Example() { let [name, setName] = React.useState(''); let onSubmit = (e) => { e.preventDefault(); // Submit data to your backend API... alert(name); }; return ( <Form onSubmit={onSubmit}> <TextField value={name} onChange={setName}> <Label>Name</Label> <Input /> </TextField> <div>You entered: {name}</div> <Button type="submit">Submit</Button> </Form> ); } import { Button, Form, Input, Label, TextField } from 'react-aria-components'; function Example() { let [name, setName] = React.useState(''); let onSubmit = (e) => { e.preventDefault(); // Submit data to your backend API... alert(name); }; return ( <Form onSubmit={onSubmit}> <TextField value={name} onChange={setName}> <Label>Name</Label> <Input /> </TextField> <div>You entered: {name}</div> <Button type="submit">Submit</Button> </Form> ); } import { Button, Form, Input, Label, TextField } from 'react-aria-components'; function Example() { let [name, setName] = React.useState(''); let onSubmit = (e) => { e.preventDefault(); // Submit data to your backend API... alert(name); }; return ( <Form onSubmit={onSubmit} > <TextField value={name} onChange={setName} > <Label> Name </Label> <Input /> </TextField> <div> You entered:{' '} {name} </div> <Button type="submit"> Submit </Button> </Form> ); } Name You entered: Submit ## Validation# * * * Form validation is important to ensure user input is in an expected format and meets business requirements. Well-designed form validation assists the user with specific, helpful error messages without confusing and frustrating them with unnecessary errors on partial input. React Aria supports native HTML constraint validation with customizable UI, custom validation functions, realtime validation, and integration with server-side validation errors. ### Built-in validation# All React Aria form components integrate with native HTML constraint validation. This allows you to define constraints on each field such as required, minimum and maximum values, text formats such as email addresses, and even custom regular expression patterns. These constraints are checked by the browser when the user commits changes to the value (e.g. on blur) or submits the form. React Aria's `FieldError` component makes it easy to display validation errors with custom styles rather than the browser's default UI. This example shows a required email field, which is validated by the browser and displayed with a custom UI. import {FieldError} from 'react-aria-components'; <Form> <TextField name="email" type="email" isRequired> <Label>Email</Label> <Input /> <FieldError /> </TextField> <Button type="submit">Submit</Button> </Form> import {FieldError} from 'react-aria-components'; <Form> <TextField name="email" type="email" isRequired> <Label>Email</Label> <Input /> <FieldError /> </TextField> <Button type="submit">Submit</Button> </Form> import {FieldError} from 'react-aria-components'; <Form> <TextField name="email" type="email" isRequired > <Label> Email </Label> <Input /> <FieldError /> </TextField> <Button type="submit"> Submit </Button> </Form> Email Submit Supported constraints include: * `isRequired` indicates that a field must have a value before the form can be submitted. * `minValue` and `maxValue` specify the minimum and maximum value in a date picker or number field. * `minLength` and `maxLength` specify the minimum and length of text input. * `pattern` provides a custom regular expression that a text input must conform to. * `type="email"` and `type="url"` provide builtin validation for email addresses and URLs. See each component's documentation for more details on the supported validation props. ### Customizing error messages# By default, the `FieldError` component displays the error message provided by the browser, which is localized in the user's preferred language. You can customize these messages by providing a render prop function to `FieldError`. This receives a list of error strings along with a ValidityState object describing why the field is invalid. <Form> <TextField name="name" isRequired> <Label>Name</Label> <Input /> <FieldError> {({validationDetails}) => ( validationDetails.valueMissing ? 'Please enter a name.' : '' )} </FieldError> </TextField> <Button type="submit">Submit</Button> </Form> <Form> <TextField name="name" isRequired> <Label>Name</Label> <Input /> <FieldError> {({ validationDetails }) => ( validationDetails.valueMissing ? 'Please enter a name.' : '' )} </FieldError> </TextField> <Button type="submit">Submit</Button> </Form> <Form> <TextField name="name" isRequired > <Label>Name</Label> <Input /> <FieldError> {( { validationDetails } ) => ( validationDetails .valueMissing ? 'Please enter a name.' : '' )} </FieldError> </TextField> <Button type="submit"> Submit </Button> </Form> Name Submit **Note**: The default error messages are localized by the browser using the browser/operating system language setting. React Aria's I18nProvider has no effect on validation errors. ### Custom validation# In addition to the built-in constraints, custom validation is supported by providing a function to the `validate` prop. This function receives the current field value, and can return a string or array of strings representing one or more error messages. These are displayed to the user after the value is committed (e.g. on blur) to avoid distracting them on each keystroke. <Form> <TextField validate={value => value === 'admin' ? 'Nice try!' : null}> <Label>Username</Label> <Input /> <FieldError /> </TextField> <Button type="submit">Submit</Button> </Form> <Form> <TextField validate={(value) => value === 'admin' ? 'Nice try!' : null} > <Label>Username</Label> <Input /> <FieldError /> </TextField> <Button type="submit">Submit</Button> </Form> <Form> <TextField validate={(value) => value === 'admin' ? 'Nice try!' : null} > <Label> Username </Label> <Input /> <FieldError /> </TextField> <Button type="submit"> Submit </Button> </Form> Username Submit ### Realtime validation# By default, validation errors are displayed to the user after the value is committed (e.g. on blur), or when the form is submitted. This avoids confusing the user with irrelevant errors while they are still entering a value. In some cases, validating in realtime can be desireable, such as when meeting password requirements. This can be accomplished by making the field value controlled, and setting the `isInvalid` prop and `FieldError` children accordingly. function Example() { let [password, setPassword] = React.useState(''); let errors = []; if (password.length < 8) { errors.push('Password must be 8 characters or more.'); } if ((password.match(/[A-Z]/g) ?? []).length < 2) { errors.push('Password must include at least 2 upper case letters'); } if ((password.match(/[^a-z]/ig) ?? []).length < 2) { errors.push('Password must include at least 2 symbols.'); } return ( <TextField isInvalid={errors.length > 0} value={password} onChange={setPassword} > <Label>Name</Label> <Input /> <FieldError> <ul>{errors.map((error, i) => <li key={i}>{error}</li>)}</ul> </FieldError> </TextField> ); } function Example() { let [password, setPassword] = React.useState(''); let errors = []; if (password.length < 8) { errors.push('Password must be 8 characters or more.'); } if ((password.match(/[A-Z]/g) ?? []).length < 2) { errors.push( 'Password must include at least 2 upper case letters' ); } if ((password.match(/[^a-z]/ig) ?? []).length < 2) { errors.push( 'Password must include at least 2 symbols.' ); } return ( <TextField isInvalid={errors.length > 0} value={password} onChange={setPassword} > <Label>Name</Label> <Input /> <FieldError> <ul> {errors.map((error, i) => <li key={i}>{error} </li>)} </ul> </FieldError> </TextField> ); } function Example() { let [ password, setPassword ] = React.useState(''); let errors = []; if ( password.length < 8 ) { errors.push( 'Password must be 8 characters or more.' ); } if ( (password.match( /[A-Z]/g ) ?? []).length < 2 ) { errors.push( 'Password must include at least 2 upper case letters' ); } if ( (password.match( /[^a-z]/ig ) ?? []).length < 2 ) { errors.push( 'Password must include at least 2 symbols.' ); } return ( <TextField isInvalid={errors .length > 0} value={password} onChange={setPassword} > <Label>Name</Label> <Input /> <FieldError> <ul> {errors.map(( error, i ) => ( <li key={i}> {error} </li> ))} </ul> </FieldError> </TextField> ); } Name * Password must be 8 characters or more. * Password must include at least 2 upper case letters * Password must include at least 2 symbols. By default, invalid fields block forms from being submitted. To avoid this, use `validationBehavior="aria"`, which will only mark the field as required and invalid for assistive technologies, and will not prevent form submission. ### Server validation# Client side validation is useful to give the user immediate feedback, but only one half of the validation story. Data should also be validated on the backend for security and reliability, and your business logic may include rules which cannot be validated on the frontend. React Aria supports displaying server validation errors by passing the `validationErrors` prop to the Form component. This should be set to an object that maps each field's `name` prop to a string or array of strings representing one or more errors. These are displayed to the user as soon as the `validationErrors` prop is set, and cleared after the user modifies each field's value. function Example() { let [errors, setErrors] = React.useState({}); let onSubmit = async (e: React.FormEvent<HTMLFormElement>) => { e.preventDefault(); let data = Object.fromEntries(new FormData(e.currentTarget)); let result = await callServer(data) setErrors(result.errors); }; return ( <Form validationErrors={errors} onSubmit={onSubmit}> <TextField name="username" isRequired> <Label>Username</Label> <Input /> <FieldError /> </TextField> <TextField name="password" type="password" isRequired> <Label>Password</Label> <Input /> <FieldError /> </TextField> <Button type="submit">Submit</Button> </Form> ); } // Fake server used in this example. function callServer(data) { return { errors: { username: 'Sorry, this username is taken.' } }; } function Example() { let [errors, setErrors] = React.useState({}); let onSubmit = async ( e: React.FormEvent<HTMLFormElement> ) => { e.preventDefault(); let data = Object.fromEntries( new FormData(e.currentTarget) ); let result = await callServer(data); setErrors(result.errors); }; return ( <Form validationErrors={errors} onSubmit={onSubmit}> <TextField name="username" isRequired> <Label>Username</Label> <Input /> <FieldError /> </TextField> <TextField name="password" type="password" isRequired> <Label>Password</Label> <Input /> <FieldError /> </TextField> <Button type="submit">Submit</Button> </Form> ); } // Fake server used in this example. function callServer(data) { return { errors: { username: 'Sorry, this username is taken.' } }; } function Example() { let [ errors, setErrors ] = React.useState({}); let onSubmit = async ( e: React.FormEvent< HTMLFormElement > ) => { e.preventDefault(); let data = Object .fromEntries( new FormData( e.currentTarget ) ); let result = await callServer( data ); setErrors( result.errors ); }; return ( <Form validationErrors={errors} onSubmit={onSubmit} > <TextField name="username" isRequired > <Label> Username </Label> <Input /> <FieldError /> </TextField> <TextField name="password" type="password" isRequired > <Label> Password </Label> <Input /> <FieldError /> </TextField> <Button type="submit"> Submit </Button> </Form> ); } // Fake server used in this example. function callServer( data ) { return { errors: { username: 'Sorry, this username is taken.' } }; } Username Password Submit #### Schema validation# React Aria is compatible with errors returned from schema validation libraries like Zod, which are often used for server-side form validation. Use the flatten method to get a list of errors for each field and return this as part of your HTTP response. // In your server... import {z} from 'zod'; const schema = z.object({ name: z.string().min(1), age: z.coerce.number().positive() }); function handleRequest(formData: FormData) { let result = schema.safeParse(Object.fromEntries(formData)); if (!result.success) { return { errors: result.error.flatten().fieldErrors }; } // Do stuff... return { errors: {} }; } // In your server... import {z} from 'zod'; const schema = z.object({ name: z.string().min(1), age: z.coerce.number().positive() }); function handleRequest(formData: FormData) { let result = schema.safeParse( Object.fromEntries(formData) ); if (!result.success) { return { errors: result.error.flatten().fieldErrors }; } // Do stuff... return { errors: {} }; } // In your server... import {z} from 'zod'; const schema = z.object({ name: z.string().min( 1 ), age: z.coerce.number() .positive() }); function handleRequest( formData: FormData ) { let result = schema .safeParse( Object.fromEntries( formData ) ); if (!result.success) { return { errors: result.error .flatten() .fieldErrors }; } // Do stuff... return { errors: {} }; } Note that error message localization is also best done on the server rather than on the client to avoid large bundles. You can submit the user's locale as part of the form data if needed, and use something like zod-i18n to translate the errors into the correct language. #### React Server Actions# Server Actions are a new React feature currently supported by Next.js. They enable the client to seamlessly call the server without setting up any API routes, and integrate with forms via the `action` prop. The useFormState hook can be used to get the value returned by a server action after submitting a form, which may include validation errors. // app/add-form.tsx 'use client'; import {useFormState} from 'react-dom'; import {Button, FieldError, Form, Input, Label, TextField} from 'react-aria-components'; import {createTodo} from '@/app/actions'; export function AddForm() { let [{ errors }, formAction] = useFormState(createTodo, { errors: {} }); return ( <Form action={formAction} validationErrors={errors}> <TextField name="todo"> <Label>Task</Label> <Input /> <FieldError /> </TextField> <Button type="submit">Add</Button> </Form> ); } // app/add-form.tsx 'use client'; import {useFormState} from 'react-dom'; import { Button, FieldError, Form, Input, Label, TextField } from 'react-aria-components'; import {createTodo} from '@/app/actions'; export function AddForm() { let [{ errors }, formAction] = useFormState(createTodo, { errors: {} }); return ( <Form action={formAction} validationErrors={errors}> <TextField name="todo"> <Label>Task</Label> <Input /> <FieldError /> </TextField> <Button type="submit">Add</Button> </Form> ); } // app/add-form.tsx 'use client'; import {useFormState} from 'react-dom'; import { Button, FieldError, Form, Input, Label, TextField } from 'react-aria-components'; import {createTodo} from '@/app/actions'; export function AddForm() { let [ { errors }, formAction ] = useFormState( createTodo, { errors: {} } ); return ( <Form action={formAction} validationErrors={errors} > <TextField name="todo"> <Label> Task </Label> <Input /> <FieldError /> </TextField> <Button type="submit"> Add </Button> </Form> ); } // app/actions.ts 'use server'; export async function createTodo(prevState: any, formData: FormData) { try { // Create the todo... } catch (err) { return { errors: { todo: 'Invalid todo.' } }; } } // app/actions.ts 'use server'; export async function createTodo( prevState: any, formData: FormData ) { try { // Create the todo... } catch (err) { return { errors: { todo: 'Invalid todo.' } }; } } // app/actions.ts 'use server'; export async function createTodo( prevState: any, formData: FormData ) { try { // Create the todo... } catch (err) { return { errors: { todo: 'Invalid todo.' } }; } } #### Remix# Remix actions handle form submissions on the server. The useSubmit hook can be used to submit data to the server, and the useActionData hook can be used to get the value returned by the server, which may include validation errors. // app/routes/signup.tsx import type {ActionFunctionArgs} from '@remix-run/node'; import {useActionData, useSubmit} from '@remix-run/react'; import {Button, FieldError, Form, Input, Label, TextField} from 'react-aria-components'; export default function SignupForm() { let submit = useSubmit(); let onSubmit = (e: React.FormEvent<HTMLFormElement>) => { e.preventDefault(); submit(e.currentTarget); }; let actionData = useActionData<typeof action>(); return ( <Form method="post" validationErrors={actionData?.errors} onSubmit={onSubmit} > <TextField name="username" isRequired> <Label>Username</Label> <Input /> <FieldError /> </TextField> <TextField name="password" type="password" isRequired> <Label>Password</Label> <Input /> <FieldError /> </TextField> <Button type="submit">Submit</Button> </Form> ); } export async function action({ request }: ActionFunctionArgs) { try { // Validate data and perform action... } catch (err) { return { errors: { username: 'Sorry, this username is taken.' } }; } } // app/routes/signup.tsx import type {ActionFunctionArgs} from '@remix-run/node'; import {useActionData, useSubmit} from '@remix-run/react'; import { Button, FieldError, Form, Input, Label, TextField } from 'react-aria-components'; export default function SignupForm() { let submit = useSubmit(); let onSubmit = (e: React.FormEvent<HTMLFormElement>) => { e.preventDefault(); submit(e.currentTarget); }; let actionData = useActionData<typeof action>(); return ( <Form method="post" validationErrors={actionData?.errors} onSubmit={onSubmit} > <TextField name="username" isRequired> <Label>Username</Label> <Input /> <FieldError /> </TextField> <TextField name="password" type="password" isRequired> <Label>Password</Label> <Input /> <FieldError /> </TextField> <Button type="submit">Submit</Button> </Form> ); } export async function action( { request }: ActionFunctionArgs ) { try { // Validate data and perform action... } catch (err) { return { errors: { username: 'Sorry, this username is taken.' } }; } } // app/routes/signup.tsx import type {ActionFunctionArgs} from '@remix-run/node'; import { useActionData, useSubmit } from '@remix-run/react'; import { Button, FieldError, Form, Input, Label, TextField } from 'react-aria-components'; export default function SignupForm() { let submit = useSubmit(); let onSubmit = ( e: React.FormEvent< HTMLFormElement > ) => { e.preventDefault(); submit( e.currentTarget ); }; let actionData = useActionData< typeof action >(); return ( <Form method="post" validationErrors={actionData ?.errors} onSubmit={onSubmit} > <TextField name="username" isRequired > <Label> Username </Label> <Input /> <FieldError /> </TextField> <TextField name="password" type="password" isRequired > <Label> Password </Label> <Input /> <FieldError /> </TextField> <Button type="submit"> Submit </Button> </Form> ); } export async function action( { request }: ActionFunctionArgs ) { try { // Validate data and perform action... } catch (err) { return { errors: { username: 'Sorry, this username is taken.' } }; } } ## Form libraries# * * * In most cases, uncontrolled forms with the builtin validation features are enough. However, if you are building a truly complex form, or integrating React Aria components into an existing form, a separate form library such as React Hook Form or Formik may be helpful. ### React Hook Form# React Hook Form is a popular form library for React. It is primarily designed to work directly with plain HTML input elements, but supports custom form components like the ones in React Aria as well. Since React Aria manages the state for components internally, you can use the Controller component from React Hook Form to integrate React Aria components. Pass the props for the `field` render prop through to the React Aria component you're using, and use the `fieldState` to get validation errors to display. import {Controller, useForm} from 'react-hook-form'; import {Button, FieldError, Form, Input, Label, TextField} from 'react-aria-components'; function App() { let { handleSubmit, control } = useForm({ defaultValues: { name: '' } }); let onSubmit = (data) => { // Call your API here... }; return ( <Form onSubmit={handleSubmit(onSubmit)}> <Controller control={control} name="name" rules={{ required: 'Name is required.' }} render={({ field: { name, value, onChange, onBlur, ref }, fieldState: { invalid, error } }) => ( <TextField name={name} value={value} onChange={onChange} onBlur={onBlur} isRequired // Let React Hook Form handle validation instead of the browser. validationBehavior="aria" isInvalid={invalid} > <Label>Name</Label> // Assign React Hook Form ref to Input so it can focus the Input after validation. <Input ref={ref} /> <FieldError>{error?.message}</FieldError> </TextField> )} /> <Button type="submit">Submit</Button> </Form> ); } import {Controller, useForm} from 'react-hook-form'; import { Button, FieldError, Form, Input, Label, TextField } from 'react-aria-components'; function App() { let { handleSubmit, control } = useForm({ defaultValues: { name: '' } }); let onSubmit = (data) => { // Call your API here... }; return ( <Form onSubmit={handleSubmit(onSubmit)}> <Controller control={control} name="name" rules={{ required: 'Name is required.' }} render={({ field: { name, value, onChange, onBlur, ref }, fieldState: { invalid, error } }) => ( <TextField name={name} value={value} onChange={onChange} onBlur={onBlur} isRequired // Let React Hook Form handle validation instead of the browser. validationBehavior="aria" isInvalid={invalid} > <Label>Name</Label> // Assign React Hook Form ref to Input so it can focus the Input after validation. <Input ref={ref} /> <FieldError>{error?.message}</FieldError> </TextField> )} /> <Button type="submit">Submit</Button> </Form> ); } import { Controller, useForm } from 'react-hook-form'; import { Button, FieldError, Form, Input, Label, TextField } from 'react-aria-components'; function App() { let { handleSubmit, control } = useForm({ defaultValues: { name: '' } }); let onSubmit = ( data ) => { // Call your API here... }; return ( <Form onSubmit={handleSubmit( onSubmit )} > <Controller control={control} name="name" rules={{ required: 'Name is required.' }} render={({ field: { name, value, onChange, onBlur, ref }, fieldState: { invalid, error } }) => ( <TextField name={name} value={value} onChange={onChange} onBlur={onBlur} isRequired // Let React Hook Form handle validation instead of the browser. validationBehavior="aria" isInvalid={invalid} > <Label> Name </Label> // Assign React Hook Form ref to Input so it can focus the Input after validation. <Input ref={ref} /> <FieldError> {error ?.message} </FieldError> </TextField> )} /> <Button type="submit"> Submit </Button> </Form> ); } ## Hooks# * * * If you're using React Aria hooks rather than components, native form validation can be enabled using the `validationBehavior="native"` prop. Each hook returns validation information which can be used to render error messages with custom styles. let {isInvalid, validationErrors, validationDetails} = useTextField(props, ref); let { isInvalid, validationErrors, validationDetails } = useTextField(props, ref); let { isInvalid, validationErrors, validationDetails } = useTextField( props, ref ); | Name | Type | Description | | --- | --- | --- | | `isInvalid` | `boolean` | Whether the input value is invalid. | | `validationErrors` | `string[]` | The current error messages for the input if it is invalid, otherwise an empty array. | | `validationDetails` | `ValidityState` | The native validation details for the input. | Server errors can be provided using the `FormValidationContext` directly, or using the `Form` component from `react-aria-components` as described above. import {FormValidationContext} from 'react-stately'; <FormValidationContext.Provider value={{ username: 'This username is taken.' }} > <MyTextField name="username" isRequired validationBehavior="native" /> {/* ... */} </FormValidationContext.Provider> import {FormValidationContext} from 'react-stately'; <FormValidationContext.Provider value={{ username: 'This username is taken.' }} > <MyTextField name="username" isRequired validationBehavior="native" /> {/* ... */} </FormValidationContext.Provider> import {FormValidationContext} from 'react-stately'; <FormValidationContext.Provider value={{ username: 'This username is taken.' }} > <MyTextField name="username" isRequired validationBehavior="native" /> {/* ... */} </FormValidationContext.Provider> See the useTextField docs for an example of how to render validation errors. --- ## Page: https://react-spectrum.adobe.com/react-aria/collections.html # Collections Many components display a collection of items, and provide functionality such as keyboard navigation, selection, and more. React Aria has a consistent, compositional API to define the items displayed in these components. ## Introduction# * * * Many React Aria components display a collection of items of some kind. For example, lists, menus, selects, tables, trees, and grids. These collections can usually be navigated with the keyboard using arrow keys, and many have some form of selection. Many support loading data asynchronously, updating that data over time, virtualized scrolling for performance with large collections, and more. React Aria implements a JSX-based API for defining collections. This is an intuitive way to provide items with rich contents and various options as props. Building hierarchical collections, e.g. sections, or a tree of items is also very natural in JSX. React Aria provides a consistent API across many types of collection components that is easy to learn, performant with large collections, and extensible for advanced features. ## Static collections# * * * A **static collection** is a collection that does not change over time (e.g. hard coded). This is common for components like action menus where the items are built into the application rather than representing user data. <Menu> <MenuItem>Open</MenuItem> <MenuItem>Edit</MenuItem> <MenuItem>Delete</MenuItem> </Menu> <Menu> <MenuItem>Open</MenuItem> <MenuItem>Edit</MenuItem> <MenuItem>Delete</MenuItem> </Menu> <Menu> <MenuItem> Open </MenuItem> <MenuItem> Edit </MenuItem> <MenuItem> Delete </MenuItem> </Menu> ### Sections# Sections or groups of items can be constructed by wrapping the items in a section element. A `<Header>` can also be rendered within a section to provide a section title. <Menu> <MenuSection> <Header>Styles</Header> <MenuItem>Bold</MenuItem> <MenuItem>Underline</MenuItem> </MenuSection> <MenuSection> <Header>Align</Header> <MenuItem>Left</MenuItem> <MenuItem>Middle</MenuItem> <MenuItem>Right</MenuItem> </MenuSection> </Menu> <Menu> <MenuSection> <Header>Styles</Header> <MenuItem>Bold</MenuItem> <MenuItem>Underline</MenuItem> </MenuSection> <MenuSection> <Header>Align</Header> <MenuItem>Left</MenuItem> <MenuItem>Middle</MenuItem> <MenuItem>Right</MenuItem> </MenuSection> </Menu> <Menu> <MenuSection> <Header> Styles </Header> <MenuItem> Bold </MenuItem> <MenuItem> Underline </MenuItem> </MenuSection> <MenuSection> <Header> Align </Header> <MenuItem> Left </MenuItem> <MenuItem> Middle </MenuItem> <MenuItem> Right </MenuItem> </MenuSection> </Menu> ## Dynamic collections# * * * Static collections are great when the items never change, but how about dynamic data? A **dynamic collection** is a collection that is based on data, for example from an API. In addition, it may change over time as items are added, updated, or removed from the collection by a user. React Aria implements a JSX-based interface for dynamic collections, which maps over your data and applies a function for each item to render it. The following example shows how a collection can be rendered based on dynamic data, stored in React state. let [animals, setAnimals] = useState([ {id: 1, name: 'Aardvark'}, {id: 2, name: 'Kangaroo'}, {id: 3, name: 'Snake'} ]); <ListBox items={animals}> {item => <ListBoxItem>{item.name}</ListBoxItem>} </ListBox> let [animals, setAnimals] = useState([ {id: 1, name: 'Aardvark'}, {id: 2, name: 'Kangaroo'}, {id: 3, name: 'Snake'} ]); <ListBox items={animals}> {item => <ListBoxItem>{item.name}</ListBoxItem>} </ListBox> let [ animals, setAnimals ] = useState([ { id: 1, name: 'Aardvark' }, { id: 2, name: 'Kangaroo' }, { id: 3, name: 'Snake' } ]); <ListBox items={animals} > {(item) => ( <ListBoxItem> {item.name} </ListBoxItem> )} </ListBox> As you can see, the items are passed to the `items` prop of the top-level component, which iterates over each item and calls the function passed as children to the component. The item object is passed to the function, which returns a `<ListBoxItem>`. ### Unique ids# All items in a collection must have a unique id, which is used to determine what items in the collection changed when updates occur. By default, React Aria looks for an `id` property on each item object, which is often returned from a database. You can also specify a custom id on each item element using the `id` prop. For example, if all animals in the example had a unique `name` property, then each item's `id` could be set to `item.name` to use it as the unique id. let [animals, setAnimals] = useState([ {name: 'Aardvark'}, {name: 'Kangaroo'}, {name: 'Snake'} ]); <ListBox items={animals}> {item => <ListBoxItem id={item.name}>{item.name}</ListBoxItem>} </ListBox> let [animals, setAnimals] = useState([ { name: 'Aardvark' }, { name: 'Kangaroo' }, { name: 'Snake' } ]); <ListBox items={animals}> {(item) => ( <ListBoxItem id={item.name}>{item.name}</ListBoxItem> )} </ListBox> let [ animals, setAnimals ] = useState([ { name: 'Aardvark' }, { name: 'Kangaroo' }, { name: 'Snake' } ]); <ListBox items={animals} > {(item) => ( <ListBoxItem id={item.name} > {item.name} </ListBoxItem> )} </ListBox> ### Why not array map?# You may be wondering why we didn't use `animals.map` in this example. In fact, this works just fine, but it's less performant, and you must remember to provide both a React `key` and an `id` prop. let [animals, setAnimals] = useState([ {name: 'Aardvark'}, {name: 'Kangaroo'}, {name: 'Snake'} ]); <ListBox> {animals.map(item => <ListBoxItem key={item.name} id={item.name}>{item.name}</ListBoxItem> )} </ListBox> let [animals, setAnimals] = useState([ { name: 'Aardvark' }, { name: 'Kangaroo' }, { name: 'Snake' } ]); <ListBox> {animals.map((item) => ( <ListBoxItem key={item.name} id={item.name}> {item.name} </ListBoxItem> ))} </ListBox> let [ animals, setAnimals ] = useState([ { name: 'Aardvark' }, { name: 'Kangaroo' }, { name: 'Snake' } ]); <ListBox> {animals.map( (item) => ( <ListBoxItem key={item.name} id={item.name} > {item.name} </ListBoxItem> ) )} </ListBox> Using the `items` prop and providing a render function allows React Aria to automatically cache the results of rendering each item and avoid re-rendering all items in the collection when only one of them changes. This has big performance benefits for large collections. ### Updating data# When you need to update the data to add, remove, or change an item, you can do so using a standard React state update. **Important**: all items passed to a collection component must be immutable. Changing a property on an item, or calling `array.push()` or other mutating methods will not work as expected. The `useListData` hook can be used to manage the data and state for a list of items, and update it over time. It will also handle removing items from the selection state when they are removed from the list. See the useListData docs for more details. The following example shows how you might append a new item to the list. import {useListData} from 'react-stately'; let list = useListData({ initialItems: [ { name: 'Aardvark' }, { name: 'Kangaroo' }, { name: 'Snake' } ], initialSelectedKeys: ['Kangaroo'], getKey: (item) => item.name }); function addAnimal(name) { list.append({ name }); } <ListBox items={list.items}> {(item) => <ListBoxItem id={item.name}>{item.name}</ListBoxItem>} </ListBox> import {useListData} from 'react-stately'; let list = useListData({ initialItems: [ { name: 'Aardvark' }, { name: 'Kangaroo' }, { name: 'Snake' } ], initialSelectedKeys: ['Kangaroo'], getKey: (item) => item.name }); function addAnimal(name) { list.append({ name }); } <ListBox items={list.items}> {(item) => ( <ListBoxItem id={item.name}>{item.name}</ListBoxItem> )} </ListBox> import {useListData} from 'react-stately'; let list = useListData({ initialItems: [ { name: 'Aardvark' }, { name: 'Kangaroo' }, { name: 'Snake' } ], initialSelectedKeys: [ 'Kangaroo' ], getKey: (item) => item.name }); function addAnimal( name ) { list.append({ name }); } <ListBox items={list.items} > {(item) => ( <ListBoxItem id={item.name} > {item.name} </ListBoxItem> )} </ListBox> Note that `useListData` is a convenience hook, not a requirement. You can use any state management library to manage collection items. ### Dependencies# As described above, dynamic collections are automatically memoized to improve performance. Rendered item elements are cached based on the object identity of the list item. If rendering an item depends on additional external state, the `dependencies` prop must be provided. This invalidates rendered elements similar to dependencies in React's `useMemo` hook. function Example(props) { return ( <ListBox items={items} dependencies={[props.layout]}> {item => <MyItem layout={props.layout}>{item.name}</MyItem>} </ListBox> ); } function Example(props) { return ( <ListBox items={items} dependencies={[props.layout]}> {(item) => ( <MyItem layout={props.layout}>{item.name}</MyItem> )} </ListBox> ); } function Example(props) { return ( <ListBox items={items} dependencies={[ props.layout ]} > {(item) => ( <MyItem layout={props .layout} > {item.name} </MyItem> )} </ListBox> ); } Note that adding dependencies will result in the _entire_ list being invalidated when a dependency changes. To avoid this and invalidate only an individual item, update the item object itself to include the information rather than accessing it from external state. ### Sections# Sections can be built by returning a section instead of an item from the top-level item renderer. Sections also support an `items` prop and a render function for their children. If the section also has a header, the `Collection` component can be used to render the child items. let [sections, setSections] = useState([ { name: 'People', items: [ {name: 'David'}, {name: 'Same'}, {name: 'Jane'} ] }, { name: 'Animals', items: [ {name: 'Aardvark'}, {name: 'Kangaroo'}, {name: 'Snake'} ] } ]); <ListBox items={sections}> {section => <ListBoxSection id={section.name}> <Header>{section.name}</Header> <Collection items={section.children}> {item => <ListBoxItem id={item.name}>{item.name}</ListBoxItem>} </Collection> </ListBoxSection> } </ListBox> let [sections, setSections] = useState([ { name: 'People', items: [ { name: 'David' }, { name: 'Same' }, { name: 'Jane' } ] }, { name: 'Animals', items: [ { name: 'Aardvark' }, { name: 'Kangaroo' }, { name: 'Snake' } ] } ]); <ListBox items={sections}> {(section) => ( <ListBoxSection id={section.name}> <Header>{section.name}</Header> <Collection items={section.children}> {(item) => ( <ListBoxItem id={item.name}> {item.name} </ListBoxItem> )} </Collection> </ListBoxSection> )} </ListBox> let [ sections, setSections ] = useState([ { name: 'People', items: [ { name: 'David' }, { name: 'Same' }, { name: 'Jane' } ] }, { name: 'Animals', items: [ { name: 'Aardvark' }, { name: 'Kangaroo' }, { name: 'Snake' } ] } ]); <ListBox items={sections} > {(section) => ( <ListBoxSection id={section.name} > <Header> {section.name} </Header> <Collection items={section .children} > {(item) => ( <ListBoxItem id={item .name} > {item.name} </ListBoxItem> )} </Collection> </ListBoxSection> )} </ListBox> When updating nested data, be sure that all parent items change accordingly. Items are immutable, so don't use mutating methods like push, or replace a property on a parent item. Instead, copy the items that changed as needed. #### useTreeData# The `useTreeData` hook can be used to manage data and state for a tree of items. This is similar to `useListData`, but with support for hierarchical data. Like `useListData`, `useTreeData` will also handle automatically removing items from the selection when they are removed from the list. See the useTreeData docs for more details. import {useTreeData} from 'react-stately'; let tree = useTreeData({ initialItems: [ { name: 'People', items: [ { name: 'David' }, { name: 'Sam' }, { name: 'Jane' } ] }, { name: 'Animals', items: [ { name: 'Aardvark' }, { name: 'Kangaroo' }, { name: 'Snake' } ] } ], getKey: (item) => item.name, getChildren: (item) => item.items }); function addPerson(name) { tree.append('People', { name }); } <ListBox items={tree.items}> {(node) => ( <ListBoxSection id={section.name} items={node.children}> <Header>{section.name}</Header> <Collection items={section.children}> {(item) => <ListBoxItem id={item.name}>{item.name}</ListBoxItem>} </Collection> </ListBoxSection> )} </ListBox> import {useTreeData} from 'react-stately'; let tree = useTreeData({ initialItems: [ { name: 'People', items: [ { name: 'David' }, { name: 'Sam' }, { name: 'Jane' } ] }, { name: 'Animals', items: [ { name: 'Aardvark' }, { name: 'Kangaroo' }, { name: 'Snake' } ] } ], getKey: (item) => item.name, getChildren: (item) => item.items }); function addPerson(name) { tree.append('People', { name }); } <ListBox items={tree.items}> {(node) => ( <ListBoxSection id={section.name} items={node.children} > <Header>{section.name}</Header> <Collection items={section.children}> {(item) => ( <ListBoxItem id={item.name}> {item.name} </ListBoxItem> )} </Collection> </ListBoxSection> )} </ListBox> import {useTreeData} from 'react-stately'; let tree = useTreeData({ initialItems: [ { name: 'People', items: [ { name: 'David' }, { name: 'Sam' }, { name: 'Jane' } ] }, { name: 'Animals', items: [ { name: 'Aardvark' }, { name: 'Kangaroo' }, { name: 'Snake' } ] } ], getKey: (item) => item.name, getChildren: (item) => item.items }); function addPerson( name ) { tree.append('People', { name }); } <ListBox items={tree.items} > {(node) => ( <ListBoxSection id={section.name} items={node .children} > <Header> {section.name} </Header> <Collection items={section .children} > {(item) => ( <ListBoxItem id={item .name} > {item.name} </ListBoxItem> )} </Collection> </ListBoxSection> )} </ListBox> Note that `useTreeData` is a utility hook, not a requirement. You can use any state management library to manage collection items. ## Asynchronous loading# * * * The `useAsyncList` hook can be used to manage async data loading from an API. Pass a `load` function to `useAsyncList`, which returns the items to render. You can use whatever data fetching library you want, or the built-in fetch API. See the useAsyncList docs for more details. This example fetches a list of Pokemon from an API and displays them in a Select. import {useAsyncList} from 'react-stately'; let list = useAsyncList({ async load({ signal }) { let res = await fetch('https://pokeapi.co/api/v2/pokemon', { signal }); let json = await res.json(); return { items: json.results }; } }); <Select> <Label>Pick a Pokemon</Label> <Button> <SelectValue /> </Button> <Popover> <ListBox items={list.items}> {(item) => <ListBoxItem id={item.name}>{item.name}</ListBoxItem>} </ListBox> </Popover> </Select> import {useAsyncList} from 'react-stately'; let list = useAsyncList({ async load({ signal }) { let res = await fetch( 'https://pokeapi.co/api/v2/pokemon', { signal } ); let json = await res.json(); return { items: json.results }; } }); <Select> <Label>Pick a Pokemon</Label> <Button> <SelectValue /> </Button> <Popover> <ListBox items={list.items}> {(item) => ( <ListBoxItem id={item.name}> {item.name} </ListBoxItem> )} </ListBox> </Popover> </Select> import {useAsyncList} from 'react-stately'; let list = useAsyncList({ async load( { signal } ) { let res = await fetch( 'https://pokeapi.co/api/v2/pokemon', { signal } ); let json = await res .json(); return { items: json.results }; } }); <Select> <Label> Pick a Pokemon </Label> <Button> <SelectValue /> </Button> <Popover> <ListBox items={list .items} > {(item) => ( <ListBoxItem id={item .name} > {item.name} </ListBoxItem> )} </ListBox> </Popover> </Select> Note that `useAsyncList` is a convenience hook, not a requirement. You can use any state management or data loading library to manage collection items. ## Virtualized scrolling# * * * Collection components like ListBox, GridList, and Table support virtualized scrolling, which is a performance optimization for large lists. Instead of rendering all items to the DOM at once, it only renders the visible items, reusing them as the user scrolls. This results in a small number of DOM elements being rendered, reducing memory usage and improving browser layout and rendering performance. Collections can be virtualized by wrapping them in a < `Virtualizer` \>, and providing a` Layout `object such as` ListLayout `or` GridLayout `. Layouts are responsible for determining the position of each item in the collection, and providing the list of visible items. When using a Virtualizer, all items are positioned by the `Layout` object, and CSS layout properties such as flexbox and grid do not apply. import {Virtualizer, ListLayout} from 'react-aria-components'; let layout = useMemo(() => new ListLayout({ rowHeight: 50 }), []); <Virtualizer layout={layout}> <ListBox items={items}> {item => <ListBoxItem>{item}</ListBoxItem>} </ListBox> </Virtualizer> import { ListLayout, Virtualizer } from 'react-aria-components'; let layout = useMemo(() => new ListLayout({ rowHeight: 50 }), []); <Virtualizer layout={layout}> <ListBox items={items}> {(item) => <ListBoxItem>{item}</ListBoxItem>} </ListBox> </Virtualizer> import { ListLayout, Virtualizer } from 'react-aria-components'; let layout = useMemo( () => new ListLayout({ rowHeight: 50 }), [] ); <Virtualizer layout={layout} > <ListBox items={items} > {(item) => ( <ListBoxItem> {item} </ListBoxItem> )} </ListBox> </Virtualizer> See the Virtualizer docs for more details. ## Advanced: Custom collection renderers# * * * Internally, `Virtualizer` is powered by a `CollectionRenderer`. Collection components delegate to a `CollectionRenderer` to render their items and sections, and to handle keyboard navigation and drag and drop interactions. Collection renderers are provided via `CollectionRendererContext`. The default `CollectionRenderer` simply renders all items to the DOM, but this can be overridden to implement custom behavior. The API includes the following properties: | Name | Type | Description | | --- | --- | --- | | `CollectionRoot` | `React.ComponentType< CollectionRootProps >` | A component that renders the root collection items. | | `CollectionBranch` | `React.ComponentType< CollectionBranchProps >` | A component that renders the child collection items. | | `isVirtualized` | `boolean` | Whether this is a virtualized collection. | | `layoutDelegate` | ` LayoutDelegate ` | A delegate object that provides layout information for items in the collection. | | `dropTargetDelegate` | ` DropTargetDelegate ` | A delegate object that provides drop targets for pointer coordinates within the collection. | The two required properties are `CollectionRoot` and `CollectionBranch`. These are React components that render the items at the root of a collection, and the children of a specific item respectively. See the Collection interface docs for details on the collection and item APIs. import type {CollectionRenderer} from 'react-aria-components'; import {CollectionRendererContext} from 'react-aria-components'; const renderer: CollectionRenderer = { CollectionRoot({collection}) { let items = []; for (let item of collection) { items.push(item.render(item)); } return items; }, CollectionBranch({collection, parent}) { let items = []; for (let item of collection.getChildren(parent.key)) { items.push(item.render(item)); } return items; } }; <CollectionRendererContext.Provider value={renderer}> <ListBox> {/* ... */} </ListBox> </CollectionRendererContext.Provider> import type {CollectionRenderer} from 'react-aria-components'; import {CollectionRendererContext} from 'react-aria-components'; const renderer: CollectionRenderer = { CollectionRoot({ collection }) { let items = []; for (let item of collection) { items.push(item.render(item)); } return items; }, CollectionBranch({ collection, parent }) { let items = []; for (let item of collection.getChildren(parent.key)) { items.push(item.render(item)); } return items; } }; <CollectionRendererContext.Provider value={renderer}> <ListBox> {/* ... */} </ListBox> </CollectionRendererContext.Provider> import type {CollectionRenderer} from 'react-aria-components'; import {CollectionRendererContext} from 'react-aria-components'; const renderer: CollectionRenderer = { CollectionRoot( { collection } ) { let items = []; for ( let item of collection ) { items.push( item.render( item ) ); } return items; }, CollectionBranch( { collection, parent } ) { let items = []; for ( let item of collection .getChildren( parent.key ) ) { items.push( item.render( item ) ); } return items; } }; <CollectionRendererContext.Provider value={renderer} > <ListBox> {/* ... */} </ListBox> </CollectionRendererContext.Provider> ### Important Requirements 1. **The value passed to `CollectionRendererContext` must be memoized.** Otherwise React will unmount and remount `CollectionRoot` and `CollectionBranch` on every render. 2. **Additional DOM elements must have a valid ARIA role.** Use `role="presentation"` for elements added only for styling purposes. Other elements must be valid within the ARIA pattern that the collection component follows. ## Hooks# * * * If you're using React Aria and React Stately hooks rather than components, the collection API is slightly different. See the React Stately collections documentation for more details. --- ## Page: https://react-spectrum.adobe.com/react-aria/selection.html # Selection Many collection components support selecting items by clicking or tapping them, or by using the keyboard. This page discusses how to handle selection events, how to control selection programmatically, and the data structures used to represent a selection. ## Multiple selection# * * * Selection is handled by the `onSelectionChange` event, which is supported on most collection components. Controlled behavior is supported by the `selectedKeys` prop, and uncontrolled behavior is supported by the `defaultSelectedKeys` prop. These props are passed to the top-level collection component, and accept a set of unique item ids. This allows marking items as selected by their id even before they are loaded, which can be useful when you know what items should be selected on initial render, before data loading has completed. Selection is represented by a Set object. You can also pass any iterable collection (e.g. an array) to the `selectedKeys` and `defaultSelectedKeys` props, but the `onSelectionChange` event will always pass back a Set. Selection is supported on both static and dynamic collections. The following example shows how to implement controlled selection behavior on a static collection, but could be applied to a dynamic collection the same way. let [selectedKeys, setSelectedKeys] = useState(new Set()); <ListBox selectedKeys={selectedKeys} onSelectionChange={setSelectedKeys}> <ListBoxItem id="one">One</ListBoxItem> <ListBoxItem id="two">Two</ListBoxItem> <ListBoxItem id="three">Three</ListBoxItem> </ListBox> let [selectedKeys, setSelectedKeys] = useState(new Set()); <ListBox selectedKeys={selectedKeys} onSelectionChange={setSelectedKeys} > <ListBoxItem id="one">One</ListBoxItem> <ListBoxItem id="two">Two</ListBoxItem> <ListBoxItem id="three">Three</ListBoxItem> </ListBox> let [ selectedKeys, setSelectedKeys ] = useState(new Set()); <ListBox selectedKeys={selectedKeys} onSelectionChange={setSelectedKeys} > <ListBoxItem id="one"> One </ListBoxItem> <ListBoxItem id="two"> Two </ListBoxItem> <ListBoxItem id="three"> Three </ListBoxItem> </ListBox> When the user selects an item, its `id` is added to the `selectedKeys` set via the `onSelectionChange` event. ## Single selection# * * * So far, we've discussed multiple selection. However, you may wish to limit selection to a single item instead. In some components, like a select or combo box, only single selection is supported. In this case, the singular `selectedKey` and `defaultSelectedKey` props are available instead of their plural variants. These accept a single id instead of a `Set` as their value, and `onSelectionChange` is also called with a single id. let [selectedKey, setSelectedKey] = useState(null); <MyComboBox selectedKey={selectedKey} onSelectionChange={setSelectedKey}> <ListBoxItem id="one">One</ListBoxItem> <ListBoxItem id="two">Two</ListBoxItem> <ListBoxItem id="three">Three</ListBoxItem> </MyComboBox> let [selectedKey, setSelectedKey] = useState(null); <MyComboBox selectedKey={selectedKey} onSelectionChange={setSelectedKey}> <ListBoxItem id="one">One</ListBoxItem> <ListBoxItem id="two">Two</ListBoxItem> <ListBoxItem id="three">Three</ListBoxItem> </MyComboBox> let [ selectedKey, setSelectedKey ] = useState(null); <MyComboBox selectedKey={selectedKey} onSelectionChange={setSelectedKey} > <ListBoxItem id="one"> One </ListBoxItem> <ListBoxItem id="two"> Two </ListBoxItem> <ListBoxItem id="three"> Three </ListBoxItem> </MyComboBox> In components which support multiple selection, you can limit the selection to a single item using the `selectionMode` prop. This continues to accept `selectedKeys` and `defaultSelectedKeys` as a `Set`, but it will only contain a single id at a time. let [selectedKeys, setSelectedKeys] = useState(new Set()); <ListBox selectionMode="single" selectedKeys={selectedKeys} onSelectionChange={setSelectedKeys}> <ListBoxItem id="one">One</ListBoxItem> <ListBoxItem id="two">Two</ListBoxItem> <ListBoxItem id="three">Three</ListBoxItem> </ListBox> let [selectedKeys, setSelectedKeys] = useState(new Set()); <ListBox selectionMode="single" selectedKeys={selectedKeys} onSelectionChange={setSelectedKeys}> <ListBoxItem id="one">One</ListBoxItem> <ListBoxItem id="two">Two</ListBoxItem> <ListBoxItem id="three">Three</ListBoxItem> </ListBox> let [ selectedKeys, setSelectedKeys ] = useState(new Set()); <ListBox selectionMode="single" selectedKeys={selectedKeys} onSelectionChange={setSelectedKeys} > <ListBoxItem id="one"> One </ListBoxItem> <ListBoxItem id="two"> Two </ListBoxItem> <ListBoxItem id="three"> Three </ListBoxItem> </ListBox> ## Dynamic data# * * * When data in a collection changes, the selection state may need to be updated accordingly. For example, if a selected item is deleted, it should be removed from the set of selected keys. You can do this yourself, or use the `useListData` and` useTreeData `hooks to handle this automatically. import {useListData} from 'react-stately'; let list = useListData({ initialItems: [ {name: 'Aardvark'}, {name: 'Kangaroo'}, {name: 'Snake'} ], initialSelectedKeys: ['Kangaroo'], getKey: item => item.name }); function removeItem() { // Removing the list item will also remove it from the selection state. list.remove('Kangaroo');} <ListBox items={list.items} selectedKeys={list.selectedKeys} onSelectionChange={list.setSelectedKeys}> {item => <ListBoxItem id={item.name}>{item.name}</ListBoxItem>} </ListBox> import {useListData} from 'react-stately'; let list = useListData({ initialItems: [ { name: 'Aardvark' }, { name: 'Kangaroo' }, { name: 'Snake' } ], initialSelectedKeys: ['Kangaroo'], getKey: (item) => item.name }); function removeItem() { // Removing the list item will also remove it from the selection state. list.remove('Kangaroo');} <ListBox items={list.items} selectedKeys={list.selectedKeys} onSelectionChange={list.setSelectedKeys} > {(item) => ( <ListBoxItem id={item.name}>{item.name}</ListBoxItem> )} </ListBox> import {useListData} from 'react-stately'; let list = useListData({ initialItems: [ { name: 'Aardvark' }, { name: 'Kangaroo' }, { name: 'Snake' } ], initialSelectedKeys: [ 'Kangaroo' ], getKey: (item) => item.name }); function removeItem() { // Removing the list item will also remove it from the selection state. list.remove( 'Kangaroo' );} <ListBox items={list.items} selectedKeys={list .selectedKeys} onSelectionChange={list .setSelectedKeys} > {(item) => ( <ListBoxItem id={item.name} > {item.name} </ListBoxItem> )} </ListBox> For more information, see useListData and useTreeData. ## Select All# * * * Some components support a checkbox to select all items in the collection, or a keyboard shortcut like ⌘ Cmd + A. This represents a selection of all items in the collection, regardless of whether or not all items have been loaded from the network. For example, when using a component with infinite scrolling support, the user will be unaware that all items are not yet loaded because it loads more transparently to them as they scroll down. For this reason, it makes sense for select all to represent all items, not just the loaded ones. When a select all event occurs, `onSelectionChange` is called with the string `"all"` rather than a set of selected keys. `selectedKeys` and `defaultSelectedKeys` can also be set to `"all"` to programmatically select all items. This represents all items in the collection, whether currently loaded or not. The application must adjust its handling of bulk actions in this case to apply to the entire collection rather than only the keys available to it locally. let [selectedKeys, setSelectedKeys] = useState(new Set()); function performBulkAction() { if (selectedKeys === 'all') { // perform action on all items } else { // perform action on selected items in selectedKeys } } <ListBox items={items} selectedKeys={selectedKeys} onSelectionChange={setSelectedKeys}> {item => <ListBoxItem>{item.name}</ListBoxItem>} </ListBox> let [selectedKeys, setSelectedKeys] = useState(new Set()); function performBulkAction() { if (selectedKeys === 'all') { // perform action on all items } else { // perform action on selected items in selectedKeys } } <ListBox items={items} selectedKeys={selectedKeys} onSelectionChange={setSelectedKeys}> {item => <ListBoxItem>{item.name}</ListBoxItem>} </ListBox> let [ selectedKeys, setSelectedKeys ] = useState(new Set()); function performBulkAction() { if ( selectedKeys === 'all' ) { // perform action on all items } else { // perform action on selected items in selectedKeys } } <ListBox items={items} selectedKeys={selectedKeys} onSelectionChange={setSelectedKeys} > {(item) => ( <ListBoxItem> {item.name} </ListBoxItem> )} </ListBox> ## Hooks# * * * If you're using React Aria and React Stately hooks rather than components, the collection and selection APIs are slightly different. See the React Stately selection documentation for more details. --- ## Page: https://react-spectrum.adobe.com/react-aria/routing.html # Client Side Routing Many React Aria components support rendering as HTML links. This page discusses how to set up your app to integrate React Aria links with your framework or client side router. ## Introduction# * * * React Aria components such as Link, Menu, Tabs, Table, and many others support rendering elements as links that perform navigation when the user interacts with them. Each component that supports link behavior accepts the `href` prop, which causes the component to render an `<a>` element. Other link DOM props such as `target` and `download` are also supported. Depending on the component, users may interact with links in different ways. For example, users can navigate between tabs using the arrow keys, or open a link in a ComboBox using the enter key. Because React Aria components accept the `href` prop rather than supporting arbitrary element overrides, they can ensure that link navigation occurs when it is appropriate for the component. By default, links perform native browser navigation when they are interacted with. However, many apps and frameworks use client side routers to avoid a full page reload when navigating between pages. The `RouterProvider` component configures all React Aria components within it to navigate using the client side router you provide. Set this up once in the root of your app, and any React Aria component with the `href` prop will automatically navigate using your router. Note that external links to different origins will not trigger client side routing, and will use native browser navigation. Additionally, if the link has a target other than `"_self"`, uses the download attribute, or the user presses modifier keys such as Command or Alt to change the default behavior, browser native navigation will occur instead of client side routing. ## RouterProvider# * * * The `RouterProvider` component accepts two props: `navigate` and `useHref`. `navigate` should be set to a function received from your router for performing a client side navigation programmatically. `useHref` is an optional prop that converts a router-specific href to a native HTML href, e.g. prepending a base path. The following example shows the general pattern. Framework-specific examples are shown below. import {RouterProvider} from 'react-aria-components'; import {useNavigate, useHref} from 'your-router'; function App() { let navigate = useNavigate(); return ( <RouterProvider navigate={navigate} useHref={useHref}> {/* ... */} </RouterProvider> ); } import {RouterProvider} from 'react-aria-components'; import {useNavigate, useHref} from 'your-router'; function App() { let navigate = useNavigate(); return ( <RouterProvider navigate={navigate} useHref={useHref}> {/* ... */} </RouterProvider> ); } import {RouterProvider} from 'react-aria-components'; import { useHref, useNavigate } from 'your-router'; function App() { let navigate = useNavigate(); return ( <RouterProvider navigate={navigate} useHref={useHref} > {/* ... */} </RouterProvider> ); } Note: if you are using React Aria hooks rather than components, you can import `RouterProvider` from `react-aria` instead. ### Router options# All React Aria link components accept a `routerOptions` prop, which is an object that is passed through to the client side router's `navigate` function as the second argument. This can be used to control any router-specific behaviors, such as scrolling, replacing instead of pushing to the history, etc. <MenuItem href="/login" routerOptions={{ replace: true }}> {/* ...*/} </MenuItem> <MenuItem href="/login" routerOptions={{ replace: true }}> {/* ...*/} </MenuItem> <MenuItem href="/login" routerOptions={{ replace: true }} > {/* ...*/} </MenuItem> When using TypeScript, you can configure the `RouterConfig` type globally so that all link components have auto complete and type safety using a type provided by your router. import type {RouterOptions} from 'your-router'; declare module 'react-aria-components' { interface RouterConfig { routerOptions: RouterOptions } } import type {RouterOptions} from 'your-router'; declare module 'react-aria-components' { interface RouterConfig { routerOptions: RouterOptions } } import type {RouterOptions} from 'your-router'; declare module 'react-aria-components' { interface RouterConfig { routerOptions: RouterOptions; } } ### React Router# The useNavigate hook from `react-router-dom` returns a `navigate` function you can pass to `RouterProvider`. The useHref hook can also be provided if you're using React Router's `basename` option. Ensure that the component that calls `useNavigate` and renders `RouterProvider` is inside the router component (e.g. `BrowserRouter`) so that it has access to React Router's internal context. The React Router `<Routes>` element should also be defined inside React Aria's `<RouterProvider>` so that links inside the rendered routes have access to the router. import {BrowserRouter, type NavigateOptions, useHref, useNavigate} from 'react-router-dom'; import {RouterProvider} from 'react-aria-components'; declare module 'react-aria-components' { interface RouterConfig { routerOptions: NavigateOptions; } } function App() { let navigate = useNavigate(); return ( <RouterProvider navigate={navigate} useHref={useHref}> {/* Your app here... */} <Routes> <Route path="/" element={<HomePage />} /> {/* ... */} </Routes> </RouterProvider> ); } <BrowserRouter> <App /> </BrowserRouter> import { BrowserRouter, type NavigateOptions, useHref, useNavigate } from 'react-router-dom'; import {RouterProvider} from 'react-aria-components'; declare module 'react-aria-components' { interface RouterConfig { routerOptions: NavigateOptions; } } function App() { let navigate = useNavigate(); return ( <RouterProvider navigate={navigate} useHref={useHref}> {/* Your app here... */} <Routes> <Route path="/" element={<HomePage />} /> {/* ... */} </Routes> </RouterProvider> ); } <BrowserRouter> <App /> </BrowserRouter> import { BrowserRouter, type NavigateOptions, useHref, useNavigate } from 'react-router-dom'; import {RouterProvider} from 'react-aria-components'; declare module 'react-aria-components' { interface RouterConfig { routerOptions: NavigateOptions; } } function App() { let navigate = useNavigate(); return ( <RouterProvider navigate={navigate} useHref={useHref} > {/* Your app here... */} <Routes> <Route path="/" element={ <HomePage /> } /> {/* ... */} </Routes> </RouterProvider> ); } <BrowserRouter> <App /> </BrowserRouter> ### Next.js# #### App router# The useRouter hook from `next/navigation` returns a router object that can be used to perform navigation. `RouterProvider` should be rendered from a client component at the root of each page or layout that includes React Aria links. You can create a new client component for this, or combine it with other top-level providers as described in the Next.js docs. // app/provider.tsx 'use client'; import {useRouter} from 'next/navigation'; import {RouterProvider} from 'react-aria-components'; declare module 'react-aria-components' { interface RouterConfig { routerOptions: NonNullable< Parameters<ReturnType<typeof useRouter>['push']>[1] >; } } export function ClientProviders({ children }) { let router = useRouter(); return ( <RouterProvider navigate={router.push}> {children} </RouterProvider> ); } // app/provider.tsx 'use client'; import {useRouter} from 'next/navigation'; import {RouterProvider} from 'react-aria-components'; declare module 'react-aria-components' { interface RouterConfig { routerOptions: NonNullable< Parameters<ReturnType<typeof useRouter>['push']>[1] >; } } export function ClientProviders({ children }) { let router = useRouter(); return ( <RouterProvider navigate={router.push}> {children} </RouterProvider> ); } // app/provider.tsx 'use client'; import {useRouter} from 'next/navigation'; import {RouterProvider} from 'react-aria-components'; declare module 'react-aria-components' { interface RouterConfig { routerOptions: NonNullable< Parameters< ReturnType< typeof useRouter >['push'] >[1] >; } } export function ClientProviders( { children } ) { let router = useRouter(); return ( <RouterProvider navigate={router .push} > {children} </RouterProvider> ); } Then, in your page or layout server component, wrap your app in the `ClientProviders` component that you defined. // app/layout.tsx import {ClientProviders} from './provider'; export default function RootLayout({children}) { return ( <html> <body> <ClientProviders>{children}</ClientProviders> </body> </html> ); } // app/layout.tsx import {ClientProviders} from './provider'; export default function RootLayout({children}) { return ( <html> <body> <ClientProviders>{children}</ClientProviders> </body> </html> ); } // app/layout.tsx import {ClientProviders} from './provider'; export default function RootLayout( { children } ) { return ( <html> <body> <ClientProviders> {children} </ClientProviders> </body> </html> ); } If you are using the Next.js basePath setting, you'll need to configure an environment variable to access it. Then, provide a custom `useHref` function to prepend it to the href for all links. // next.config.js const basePath = '...'; const nextConfig = { basePath, env: { BASE_PATH: basePath } }; // next.config.js const basePath = '...'; const nextConfig = { basePath, env: { BASE_PATH: basePath } }; // next.config.js const basePath = '...'; const nextConfig = { basePath, env: { BASE_PATH: basePath } }; // app/provider.tsx // ... export function ClientProviders({children}) { let router = useRouter(); let useHref = (href: string) => process.env.BASE_PATH + href; return ( <RouterProvider navigate={router.push} useHref={useHref}> {children} </RouterProvider> ); } // app/provider.tsx // ... export function ClientProviders({ children }) { let router = useRouter(); let useHref = (href: string) => process.env.BASE_PATH + href; return ( <RouterProvider navigate={router.push} useHref={useHref} > {children} </RouterProvider> ); } // app/provider.tsx // ... export function ClientProviders( { children } ) { let router = useRouter(); let useHref = ( href: string ) => process.env .BASE_PATH + href; return ( <RouterProvider navigate={router .push} useHref={useHref} > {children} </RouterProvider> ); } #### Pages router# The useRouter hook from `next/router` returns a router object that can be used to perform navigation. `RouterProvider` should be rendered at the root of each page that includes React Aria links, or in `pages/_app.tsx` to add it to all pages. // pages/_app.tsx import type {AppProps} from 'next/app'; import {type NextRouter, useRouter} from 'next/router'; import {RouterProvider} from 'react-aria-components'; declare module 'react-aria-components' { interface RouterConfig { routerOptions: NonNullable<Parameters<NextRouter['push']>[2]>; } } export default function MyApp({ Component, pageProps }: AppProps) { let router = useRouter(); return ( <RouterProvider navigate={(href, opts) => router.push(href, undefined, opts)} > <Component {...pageProps} /> </RouterProvider> ); } // pages/_app.tsx import type {AppProps} from 'next/app'; import {type NextRouter, useRouter} from 'next/router'; import {RouterProvider} from 'react-aria-components'; declare module 'react-aria-components' { interface RouterConfig { routerOptions: NonNullable< Parameters<NextRouter['push']>[2] >; } } export default function MyApp( { Component, pageProps }: AppProps ) { let router = useRouter(); return ( <RouterProvider navigate={(href, opts) => router.push(href, undefined, opts)} > <Component {...pageProps} /> </RouterProvider> ); } // pages/_app.tsx import type {AppProps} from 'next/app'; import { type NextRouter, useRouter } from 'next/router'; import {RouterProvider} from 'react-aria-components'; declare module 'react-aria-components' { interface RouterConfig { routerOptions: NonNullable< Parameters< NextRouter[ 'push' ] >[2] >; } } export default function MyApp( { Component, pageProps }: AppProps ) { let router = useRouter(); return ( <RouterProvider navigate={( href, opts ) => router.push( href, undefined, opts )} > <Component {...pageProps} /> </RouterProvider> ); } When using the basePath configuration option, provide a `useHref` prop to `RouterProvider` to prepend it to links automatically. // pages/_app.tsx // ... export default function MyApp({Component, pageProps}: AppProps) { let router = useRouter(); return ( <RouterProvider navigate={(href, opts) => router.push(href, undefined, opts)} useHref={(href: string) => router.basePath + href} > <Component {...pageProps} /> </RouterProvider> ); } // pages/_app.tsx // ... export default function MyApp( { Component, pageProps }: AppProps ) { let router = useRouter(); return ( <RouterProvider navigate={(href, opts) => router.push(href, undefined, opts)} useHref={(href: string) => router.basePath + href} > <Component {...pageProps} /> </RouterProvider> ); } // pages/_app.tsx // ... export default function MyApp( { Component, pageProps }: AppProps ) { let router = useRouter(); return ( <RouterProvider navigate={( href, opts ) => router.push( href, undefined, opts )} useHref={( href: string ) => router.basePath + href} > <Component {...pageProps} /> </RouterProvider> ); } ### Remix# Remix uses React Router under the hood, so the same useNavigate and useHref hooks described above also work in Remix apps. `RouterProvider` should be rendered at the root of each page that includes React Aria links, or in `app/root.tsx` to add it to all pages. See the Remix docs for more details. // app/root.tsx import {useNavigate, useHref, Outlet} from '@remix-run/react'; import type {NavigateOptions} from 'react-router-dom'; import {RouterProvider} from 'react-aria-components'; declare module 'react-aria-components' { interface RouterConfig { routerOptions: NavigateOptions } } export default function App() { let navigate = useNavigate(); return ( <html lang="en"> <head> {/* ... */} </head> <body> <RouterProvider navigate={navigate} useHref={useHref}> <Outlet /> </RouterProvider> {/* ... */} </body> </html> ); } // app/root.tsx import { Outlet, useHref, useNavigate } from '@remix-run/react'; import type {NavigateOptions} from 'react-router-dom'; import {RouterProvider} from 'react-aria-components'; declare module 'react-aria-components' { interface RouterConfig { routerOptions: NavigateOptions; } } export default function App() { let navigate = useNavigate(); return ( <html lang="en"> <head> {/* ... */} </head> <body> <RouterProvider navigate={navigate} useHref={useHref} > <Outlet /> </RouterProvider> {/* ... */} </body> </html> ); } // app/root.tsx import { Outlet, useHref, useNavigate } from '@remix-run/react'; import type {NavigateOptions} from 'react-router-dom'; import {RouterProvider} from 'react-aria-components'; declare module 'react-aria-components' { interface RouterConfig { routerOptions: NavigateOptions; } } export default function App() { let navigate = useNavigate(); return ( <html lang="en"> <head> {/* ... */} </head> <body> <RouterProvider navigate={navigate} useHref={useHref} > <Outlet /> </RouterProvider> {/* ... */} </body> </html> ); } ### TanStack Router# To use TanStack Router with React Aria, render React Aria's `RouterProvider` inside your root route. Use `router.navigate` in the `navigate` prop, and `router.buildLocation` in the `useHref` prop. You can also configure TypeScript to get autocomplete for the `href` prop by declaring the `RouterConfig` type using the types provided by TanStack Router. import {type NavigateOptions, type ToOptions, useRouter} from '@tanstack/react-router'; import {RouterProvider} from 'react-aria-components'; declare module 'react-aria-components' { interface RouterConfig { href: ToOptions; routerOptions: Omit<NavigateOptions, keyof ToOptions>; } } function RootRoute() { let router = useRouter(); return ( <RouterProvider navigate={(href, opts) => router.navigate({ ...href, ...options })} useHref={(href) => router.buildLocation(href).href} > {/* ...*/} </RouterProvider> ); } import { type NavigateOptions, type ToOptions, useRouter } from '@tanstack/react-router'; import {RouterProvider} from 'react-aria-components'; declare module 'react-aria-components' { interface RouterConfig { href: ToOptions; routerOptions: Omit<NavigateOptions, keyof ToOptions>; } } function RootRoute() { let router = useRouter(); return ( <RouterProvider navigate={(href, opts) => router.navigate({ ...href, ...options })} useHref={(href) => router.buildLocation(href).href} > {/* ...*/} </RouterProvider> ); } import { type NavigateOptions, type ToOptions, useRouter } from '@tanstack/react-router'; import {RouterProvider} from 'react-aria-components'; declare module 'react-aria-components' { interface RouterConfig { href: ToOptions; routerOptions: Omit< NavigateOptions, keyof ToOptions >; } } function RootRoute() { let router = useRouter(); return ( <RouterProvider navigate={( href, opts ) => router.navigate({ ...href, ...options })} useHref={(href) => router .buildLocation( href ).href} > {/* ...*/} </RouterProvider> ); } --- ## Page: https://react-spectrum.adobe.com/react-aria/ssr.html # Server Side Rendering This page describes how to use React Aria with server side rendering, including frameworks like Next.js, Remix, and Gatsby. ## Introduction# * * * **Server side rendering**, or SSR, is the process of rendering components to HTML on the server, rather than rendering them only on the client. **Static rendering** is a similar approach, but pre-renders pages to HTML at build time rather than on each request. These techniques can help improve perceived loading performance and SEO. React Aria supports both of these approaches, either through a custom implementation or via frameworks like Next.js, Remix, and Gatsby. ## Internationalization# * * * When using server side rendering, the application should be wrapped in an I18nProvider with an explicit `locale` prop, rather than relying on automatic locale selection. This ensures that the locale of the content rendered on the server matches the locale expected by the browser. The `Accept-Language` HTTP header, which the browser sends to the server with the user’s desired language, could be used to implement this. You could also use an in-application setting for this if available, or locale-specific URLs, for example. In addition to passing the `locale` prop to the `I18nProvider`, you should also ensure the `lang` and `dir` attributes are set on the `<html>` element for your page. import {I18nProvider, useLocale} from 'react-aria-components'; function App() { let {locale, direction} = useLocale(); return ( <html lang={locale} dir={direction}> {/* your app here */} </html> ); } <I18nProvider locale={locale}> <App /> </I18nProvider> import { I18nProvider, useLocale } from 'react-aria-components'; function App() { let { locale, direction } = useLocale(); return ( <html lang={locale} dir={direction}> {/* your app here */} </html> ); } <I18nProvider locale={locale}> <App /> </I18nProvider> import { I18nProvider, useLocale } from 'react-aria-components'; function App() { let { locale, direction } = useLocale(); return ( <html lang={locale} dir={direction} > {/* your app here */} </html> ); } <I18nProvider locale={locale} > <App /> </I18nProvider> See the internationalization docs for more information about i18n in React Aria. ### Optimizing bundle size# By default, React Aria includes translations for all 30+ supported languages. When using server side rendering, this can be optimized so that only the strings for the current user's language are sent over the network rather than the strings for all supported languages. This takes two steps: 1. Configure React Aria's build plugin for your framework to exclude all translation strings from your JavaScript bundle at build time. 2. Render React Aria's `LocalizedStringProvider` component at the root of your app. This includes the strings for the user's language in the initial HTML so that the client can access them. It also passes the locale to the client, so an `I18nProvider` is not needed. See below for framework-specific guideance. #### Next.js App Router# First, install `@react-aria/optimize-locales-plugin` with your package manager. Then, add the following to your `next.config.js`: // next.config.js const localesPlugin = require('@react-aria/optimize-locales-plugin'); module.exports = { // ... webpack(config, {isServer}) { if (!isServer) { // Don't include any locale strings in the client JS bundle. config.plugins.push(localesPlugin.webpack({locales: []})); } return config; } }; // next.config.js const localesPlugin = require( '@react-aria/optimize-locales-plugin' ); module.exports = { // ... webpack(config, { isServer }) { if (!isServer) { // Don't include any locale strings in the client JS bundle. config.plugins.push( localesPlugin.webpack({ locales: [] }) ); } return config; } }; // next.config.js const localesPlugin = require( '@react-aria/optimize-locales-plugin' ); module.exports = { // ... webpack( config, { isServer } ) { if (!isServer) { // Don't include any locale strings in the client JS bundle. config.plugins .push( localesPlugin .webpack({ locales: [] }) ); } return config; } }; Finally, add a `LocalizedStringProvider` to your root layout component. This example uses a URL parameter to get the requested locale. See the Next.js Internationalization guide to learn how to set this up. // app/[lang]/layout.tsx import {LocalizedStringProvider} from 'react-aria-components/i18n'; export default function RootLayout( {children, params: {lang}}: {children: React.ReactNode, params: {lang: string}} ) { return ( <html lang={lang}> <body> <LocalizedStringProvider locale={lang} /> {children} </body> </html> ); } // app/[lang]/layout.tsx import {LocalizedStringProvider} from 'react-aria-components/i18n'; export default function RootLayout( { children, params: { lang } }: { children: React.ReactNode; params: { lang: string }; } ) { return ( <html lang={lang}> <body> <LocalizedStringProvider locale={lang} /> {children} </body> </html> ); } // app/[lang]/layout.tsx import {LocalizedStringProvider} from 'react-aria-components/i18n'; export default function RootLayout( { children, params: { lang } }: { children: React.ReactNode; params: { lang: string; }; } ) { return ( <html lang={lang}> <body> <LocalizedStringProvider locale={lang} /> {children} </body> </html> ); } **Note**: If you are using React Aria hooks rather than components, this can be imported from `react-aria/i18n` instead. #### Next.js Pages Router# First, install `@react-aria/optimize-locales-plugin` with your package manager. Then, add the following to your `next.config.js`: // next.config.js const localesPlugin = require('@react-aria/optimize-locales-plugin'); module.exports = { // ... i18n: { // See Next.js i18n docs... }, webpack(config, {isServer}) { if (!isServer) { // Don't include any locale strings in the client JS bundle. config.plugins.push(localesPlugin.webpack({locales: []})); } return config; } }; // next.config.js const localesPlugin = require( '@react-aria/optimize-locales-plugin' ); module.exports = { // ... i18n: { // See Next.js i18n docs... }, webpack(config, { isServer }) { if (!isServer) { // Don't include any locale strings in the client JS bundle. config.plugins.push( localesPlugin.webpack({ locales: [] }) ); } return config; } }; // next.config.js const localesPlugin = require( '@react-aria/optimize-locales-plugin' ); module.exports = { // ... i18n: { // See Next.js i18n docs... }, webpack( config, { isServer } ) { if (!isServer) { // Don't include any locale strings in the client JS bundle. config.plugins .push( localesPlugin .webpack({ locales: [] }) ); } return config; } }; Finally, add a `LocalizedStringProvider` to `pages/_document.tsx` before the `<NextScript />` element. Use `props.locale` to access the requested locale, which is provided by Next.js. See the Next.js Internationalization guide to learn how to set this up. // pages/_document.tsx import {Html, Head, Main, NextScript, DocumentProps} from 'next/document' import {LocalizedStringProvider} from 'react-aria-components/i18n'; export default function Document(props: DocumentProps) { return ( <Html lang={props.locale}> <Head /> <body> <Main /> <LocalizedStringProvider locale={props.locale} /> <NextScript /> </body> </Html> ); } // pages/_document.tsx import { DocumentProps, Head, Html, Main, NextScript } from 'next/document'; import {LocalizedStringProvider} from 'react-aria-components/i18n'; export default function Document(props: DocumentProps) { return ( <Html lang={props.locale}> <Head /> <body> <Main /> <LocalizedStringProvider locale={props.locale} /> <NextScript /> </body> </Html> ); } // pages/_document.tsx import { DocumentProps, Head, Html, Main, NextScript } from 'next/document'; import {LocalizedStringProvider} from 'react-aria-components/i18n'; export default function Document( props: DocumentProps ) { return ( <Html lang={props.locale} > <Head /> <body> <Main /> <LocalizedStringProvider locale={props .locale} /> <NextScript /> </body> </Html> ); } **Note**: If you are using React Aria hooks rather than components, this can be imported from `react-aria/i18n` instead. #### Remix# Remix is supported when using Vite for builds. First, install `@react-aria/optimize-locales-plugin` with your package manager. Then, add the following to your `vite.config.ts`: // vite.config.ts import { unstable_vitePlugin as remix } from '@remix-run/dev'; import { defineConfig } from 'vite'; import localesPlugin from '@react-aria/optimize-locales-plugin'; export default defineConfig({ plugins: [ remix(), // Don't include any locale strings in the client JS bundle. {...localesPlugin.vite({locales: []}), enforce: 'pre'} ], }); // vite.config.ts import {unstable_vitePlugin as remix} from '@remix-run/dev'; import {defineConfig} from 'vite'; import localesPlugin from '@react-aria/optimize-locales-plugin'; export default defineConfig({ plugins: [ remix(), // Don't include any locale strings in the client JS bundle. { ...localesPlugin.vite({ locales: [] }), enforce: 'pre' } ] }); // vite.config.ts import {unstable_vitePlugin as remix} from '@remix-run/dev'; import {defineConfig} from 'vite'; import localesPlugin from '@react-aria/optimize-locales-plugin'; export default defineConfig( { plugins: [ remix(), // Don't include any locale strings in the client JS bundle. { ...localesPlugin .vite({ locales: [] }), enforce: 'pre' } ] } ); Finally, you'll need an entry.server.tsx file, which will handle injecting the localized strings into the initial HTML for the client to access. The below file can be generated by running `npx remix reveal`, and making the highlighted modifications. **Note**: Remix uses the `getLocalizationScript` function instead of the `LocalizedStringProvider` component to inject the strings into the initial HTML. // app/entry.server.tsx import type {EntryContext} from '@remix-run/node'; import {PassThrough} from 'node:stream'; import {createReadableStreamFromReadable} from '@remix-run/node'; import {RemixServer} from '@remix-run/react'; import {renderToPipeableStream} from 'react-dom/server'; import {getLocalizationScript} from 'react-aria-components/i18n'; const ABORT_DELAY = 5000; export default async function handleRequest( request: Request, responseStatusCode: number, responseHeaders: Headers, remixContext: EntryContext, ) { // Get the requested language (e.g. from headers, URL param, database, etc.) let lang = await getRequestedLanguageSomehow(request); return new Promise((resolve, reject) => { let {pipe, abort} = renderToPipeableStream( <RemixServer context={remixContext} url={request.url} abortDelay={ABORT_DELAY} />, { bootstrapScriptContent: getLocalizationScript(lang), onShellReady() { let body = new PassThrough(); let stream = createReadableStreamFromReadable(body); responseHeaders.set('Content-Type', 'text/html'); resolve( new Response(stream, { headers: responseHeaders, status: responseStatusCode, }) ); pipe(body); }, onShellError(error: unknown) { reject(error); }, onError(error: unknown) { responseStatusCode = 500; console.error(error); }, } ); setTimeout(abort, ABORT_DELAY); }); } // app/entry.server.tsx import type {EntryContext} from '@remix-run/node'; import {PassThrough} from 'node:stream'; import {createReadableStreamFromReadable} from '@remix-run/node'; import {RemixServer} from '@remix-run/react'; import {renderToPipeableStream} from 'react-dom/server'; import {getLocalizationScript} from 'react-aria-components/i18n'; const ABORT_DELAY = 5000; export default async function handleRequest( request: Request, responseStatusCode: number, responseHeaders: Headers, remixContext: EntryContext ) { // Get the requested language (e.g. from headers, URL param, database, etc.) let lang = await getRequestedLanguageSomehow(request); return new Promise((resolve, reject) => { let { pipe, abort } = renderToPipeableStream( <RemixServer context={remixContext} url={request.url} abortDelay={ABORT_DELAY} />, { bootstrapScriptContent: getLocalizationScript(lang), onShellReady() { let body = new PassThrough(); let stream = createReadableStreamFromReadable( body ); responseHeaders.set('Content-Type', 'text/html'); resolve( new Response(stream, { headers: responseHeaders, status: responseStatusCode }) ); pipe(body); }, onShellError(error: unknown) { reject(error); }, onError(error: unknown) { responseStatusCode = 500; console.error(error); } } ); setTimeout(abort, ABORT_DELAY); }); } // app/entry.server.tsx import type {EntryContext} from '@remix-run/node'; import {PassThrough} from 'node:stream'; import {createReadableStreamFromReadable} from '@remix-run/node'; import {RemixServer} from '@remix-run/react'; import {renderToPipeableStream} from 'react-dom/server'; import {getLocalizationScript} from 'react-aria-components/i18n'; const ABORT_DELAY = 5000; export default async function handleRequest( request: Request, responseStatusCode: number, responseHeaders: Headers, remixContext: EntryContext ) { // Get the requested language (e.g. from headers, URL param, database, etc.) let lang = await getRequestedLanguageSomehow( request ); return new Promise( ( resolve, reject ) => { let { pipe, abort } = renderToPipeableStream( <RemixServer context={remixContext} url={request .url} abortDelay={ABORT_DELAY} />, { bootstrapScriptContent: getLocalizationScript( lang ), onShellReady() { let body = new PassThrough(); let stream = createReadableStreamFromReadable( body ); responseHeaders .set( 'Content-Type', 'text/html' ); resolve( new Response( stream, { headers: responseHeaders, status: responseStatusCode } ) ); pipe(body); }, onShellError( error: unknown ) { reject( error ); }, onError( error: unknown ) { responseStatusCode = 500; console .error( error ); } } ); setTimeout( abort, ABORT_DELAY ); } ); } **Note**: If you are using React Aria hooks rather than components, this can be imported from `react-aria/i18n` instead. You should also make sure to set the `lang` attribute on the root `<html>` element in `app/root.tsx` using the same language detection logic used above. #### Advanced optimization# `LocalizedStringProvider` includes the strings for all React Aria components by default. This reduces the size a lot compared with bundling all languages, but you can also manually include a subset of the strings for only the components you use to get the smallest possible bundle size. However, this takes some careful work to ensure you include the strings for all of the components used across your app and update this list over time. Start by creating a `LocalizedStringDictionary` containing only the strings for the components you use with the `createLocalizedStringDictionary` function. This accepts a list of npm package names for the individual React Aria hooks and components you use. import {createLocalizedStringDictionary} from 'react-aria-components/i18n'; const dictionary = createLocalizedStringDictionary(['@react-aria/datepicker']); import {createLocalizedStringDictionary} from 'react-aria-components/i18n'; const dictionary = createLocalizedStringDictionary([ '@react-aria/datepicker' ]); import {createLocalizedStringDictionary} from 'react-aria-components/i18n'; const dictionary = createLocalizedStringDictionary( ['@react-aria/datepicker'] ); Then, pass this as an additional prop to `LocalizedStringProvider`: <LocalizedStringProvider locale={locale} dictionary={dictionary} /> <LocalizedStringProvider locale={locale} dictionary={dictionary} /> <LocalizedStringProvider locale={locale} dictionary={dictionary} /> Or if using Remix, pass the dictionary as an additional parameter to `getLocalizationScript`: getLocalizationScript(locale, dictionary) getLocalizationScript(locale, dictionary) getLocalizationScript( locale, dictionary ); ## SSR specific rendering# * * * You can also use the useIsSSR hook in your own components to determine whether they are running in an SSR context. This hook returns `true` both during server rendering and hydration, but updates immediately to `false` after hydration. You can use this to delay browser-specific code like media queries and feature detection until after the client has hydrated. import {useIsSSR} from 'react-aria'; function MyComponent() { let isSSR = useIsSSR(); return <span>{isSSR ? 'Server' : 'Client'}</span>; } import {useIsSSR} from 'react-aria'; function MyComponent() { let isSSR = useIsSSR(); return <span>{isSSR ? 'Server' : 'Client'}</span>; } import {useIsSSR} from 'react-aria'; function MyComponent() { let isSSR = useIsSSR(); return ( <span> {isSSR ? 'Server' : 'Client'} </span> ); } ## Automatic ID Generation# * * * When using SSR, only a single copy of React Aria can be on the page at a time. This is in contrast to client-side rendering, where multiple copies from different parts of an app can coexist. Internally, many components rely on auto-generated ids to link related elements via ARIA attributes. These ids typically use a randomly generated seed plus an incrementing counter to ensure uniqueness even when multiple instances of React Aria are on the page. With SSR, we need to ensure that these ids are consistent between the server and client. This means the counter resets on every request, and we use a consistent seed. Due to this, multiple copies of React Aria cannot be supported because the auto-generated ids would conflict. If you use React Aria’s useId hook in your own components, this will ensure the ids are consistent when server rendered. In React 16 and 17 you'll need to wrap your app in an `SSRProvider` as described above. No additional changes in each component are required to enable SSR support. ## React < 18# * * * In React 18, SSR works out of the box with no additional work. If you're using React 16 or 17, you will need to wrap your application in an SSRProvider. This signals to all nested React Aria hooks that they are being rendered in an SSR context, which ensures that the HTML generated on the server matches the DOM structure hydrated on the client. import {SSRProvider} from 'react-aria'; <SSRProvider> <App /> </SSRProvider> import {SSRProvider} from 'react-aria'; <SSRProvider> <App /> </SSRProvider> import {SSRProvider} from 'react-aria'; <SSRProvider> <App /> </SSRProvider> --- ## Page: https://react-spectrum.adobe.com/react-aria/advanced.html # Advanced Customization React Aria Components is built using a flexible and composable API that you can extend to build new patterns. If you need even more customizability, drop down to the lower level Hook-based API for even more control over rendering and behavior. Mix and match as needed. ## Contexts# * * * The React Aria Components API is designed around composition. Components are reused between patterns to build larger composite components. For example, there is no dedicated `NumberFieldIncrementButton` or `SelectPopover` component. Instead, the standalone Button and Popover components are reused within NumberField and Select. This reduces the amount of duplicate styling code you need to write and maintain, and provides powerful composition capabilities you can use in your own components. <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 Components automatically provide behavior to their children by passing event handlers and other attributes via context. For example, the increment and decrement buttons in a `NumberField` receive `onPress` handlers that update the value. Keeping each element of a component separate enables full styling, layout, and DOM structure control, and contexts ensure that accessibility and behavior are taken care of on your behalf. This architecture also enables you to reuse React Aria Components in your own custom patterns, or even replace one part of a component with your own custom implementation without rebuilding the whole pattern from scratch. ### Custom patterns# Each React Aria Component exports a corresponding context that you can use to build your own compositional APIs similar to the built-in components. You can send any prop or ref via context that you could pass to the corresponding component. The local props and ref on the component are merged with the ones passed via context, with the local props taking precedence (following the rules documented in mergeProps). This example shows a `FieldGroup` component that renders a group of text fields. The entire group can be marked as disabled via the `isDisabled` prop, which is passed to all child text fields via the `TextFieldContext` provider. import {TextFieldContext} from 'react-aria-components'; interface FieldGroupProps { children?: React.ReactNode, isDisabled?: boolean } function FieldGroup({children, isDisabled}: FieldGroupProps) { return ( <TextFieldContext.Provider value={{isDisabled}}> {children} </TextFieldContext.Provider> ); } import {TextFieldContext} from 'react-aria-components'; interface FieldGroupProps { children?: React.ReactNode; isDisabled?: boolean; } function FieldGroup( { children, isDisabled }: FieldGroupProps ) { return ( <TextFieldContext.Provider value={{ isDisabled }}> {children} </TextFieldContext.Provider> ); } import {TextFieldContext} from 'react-aria-components'; interface FieldGroupProps { children?: React.ReactNode; isDisabled?: boolean; } function FieldGroup( { children, isDisabled }: FieldGroupProps ) { return ( <TextFieldContext.Provider value={{ isDisabled }} > {children} </TextFieldContext.Provider> ); } Any `TextField` component you place inside a `FieldGroup` will automatically receive the `isDisabled` prop from the group, including those that are deeply nested inside other components. <FieldGroup isDisabled={isSubmitting}> <MyTextField label="Name" /> <MyTextField label="Email" /> <CreditCardFields /> </FieldGroup> <FieldGroup isDisabled={isSubmitting}> <MyTextField label="Name" /> <MyTextField label="Email" /> <CreditCardFields /> </FieldGroup> <FieldGroup isDisabled={isSubmitting} > <MyTextField label="Name" /> <MyTextField label="Email" /> <CreditCardFields /> </FieldGroup> The contexts consumed by each component are listed in the Advanced Customization section of their documentation, along with examples of some potential use cases. ### Slots# Some patterns include multiple instances of the same component. These use the `slot` prop to distinguish each instance. Slots are named children within a component that can receive separate behaviors and styles. Separate props can be sent to slots by providing an object with keys for each slot name to the component's context provider. This example shows a `Stepper` component with slots for its increment and decrement buttons. function Stepper({children}) { let [value, setValue] = React.useState(0); return ( <ButtonContext.Provider value={{ slots: { increment: { onPress: () => setValue(value + 1) }, decrement: { onPress: () => setValue(value - 1) } } }}> {children} </ButtonContext.Provider> ); } <Stepper> <Button slot="increment">⬆</Button> <Button slot="decrement">⬇</Button> </Stepper> function Stepper({children}) { let [value, setValue] = React.useState(0); return ( <ButtonContext.Provider value={{ slots: { increment: { onPress: () => setValue(value + 1) }, decrement: { onPress: () => setValue(value - 1) } } }}> {children} </ButtonContext.Provider> ); } <Stepper> <Button slot="increment">⬆</Button> <Button slot="decrement">⬇</Button> </Stepper> function Stepper( { children } ) { let [value, setValue] = React.useState(0); return ( <ButtonContext.Provider value={{ slots: { increment: { onPress: () => setValue( value + 1 ) }, decrement: { onPress: () => setValue( value - 1 ) } } }} > {children} </ButtonContext.Provider> ); } <Stepper> <Button slot="increment"> ⬆ </Button> <Button slot="decrement"> ⬇ </Button> </Stepper> The slots provided by each built-in React Aria component are shown in the Anatomy section of their documentation. #### Default slot# The default slot is used to provide props to a component without specifying a slot name. This approach allows you to assign a default slot to a component for its default use case and enables you to specify a slot name for a specific use case. This example shows a custom component that passes a specific class name to a standard button child and to a button child with a slot named "end". import {Button, ButtonContext, DEFAULT_SLOT} from 'react-aria-components'; function MyCustomComponent({children}) { return ( <ButtonContext.Provider value={{ slots: { [DEFAULT_SLOT]: { className: "left-button" }, end: { className: "right-button" } } }}> {children} </ButtonContext.Provider> ); } <MyCustomComponent> {/* Consumes the props passed to the default slot */} <Button>Click me</Button> </MyCustomComponent> <MyCustomComponent> {/* Consumes the props passed to the "end" slot */} <Button slot="end">Click me</Button> </MyCustomComponent> import { Button, ButtonContext, DEFAULT_SLOT } from 'react-aria-components'; function MyCustomComponent({ children }) { return ( <ButtonContext.Provider value={{ slots: { [DEFAULT_SLOT]: { className: 'left-button' }, end: { className: 'right-button' } } }} > {children} </ButtonContext.Provider> ); } <MyCustomComponent> {/* Consumes the props passed to the default slot */} <Button>Click me</Button> </MyCustomComponent> <MyCustomComponent> {/* Consumes the props passed to the "end" slot */} <Button slot="end">Click me</Button> </MyCustomComponent> import { Button, ButtonContext, DEFAULT_SLOT } from 'react-aria-components'; function MyCustomComponent( { children } ) { return ( <ButtonContext.Provider value={{ slots: { [DEFAULT_SLOT]: { className: 'left-button' }, end: { className: 'right-button' } } }} > {children} </ButtonContext.Provider> ); } <MyCustomComponent> {/* Consumes the props passed to the default slot */} <Button> Click me </Button> </MyCustomComponent> <MyCustomComponent> {/* Consumes the props passed to the "end" slot */} <Button slot="end"> Click me </Button> </MyCustomComponent> ### Provider# In complex components, you may need to provide many contexts. The `Provider` component is a utility that makes it easier to provide multiple React contexts without manually nesting them. This can be achieved by passing pairs of contexts and values as an array to the `values` prop. import {Provider, ButtonContext, InputContext} from 'react-aria-components'; <Provider values={[ [ButtonContext, {/* ... */}], [InputContext, {/* ... */}] ]}> {/* ... */} </Provider> import { ButtonContext, InputContext, Provider } from 'react-aria-components'; <Provider values={[ [ButtonContext, {/* ... */}], [InputContext, {/* ... */}] ]} > {/* ... */} </Provider> import { ButtonContext, InputContext, Provider } from 'react-aria-components'; <Provider values={[ [ButtonContext, { /* ... */ }], [InputContext, { /* ... */ }] ]} > {/* ... */} </Provider> This is equivalent to: <ButtonContext.Provider value={{/* ... */}}> <InputContext.Provider value={{/* ... */}}> {/* ... */} </InputContext.Provider> </ButtonContext.Provider> <ButtonContext.Provider value={{/* ... */}}> <InputContext.Provider value={{/* ... */}}> {/* ... */} </InputContext.Provider> </ButtonContext.Provider> <ButtonContext.Provider value={{/* ... */}} > <InputContext.Provider value={{/* ... */}} > {/* ... */} </InputContext.Provider> </ButtonContext.Provider> ### Consuming contexts# You can also consume from contexts provided by React Aria Components in your own custom components. This allows you to replace a component used as part of a larger pattern with a custom implementation. For example, you could consume from `LabelContext` in an existing styled label component to make it compatible with React Aria Components. #### useContextProps# The `useContextProps` hook merges the local props and ref with the ones provided via context by a parent component. The local props always take precedence over the context values (following the rules documented in mergeProps). `useContextProps` supports the slot prop to indicate which value to consume from context. import type {LabelProps} from 'react-aria-components'; import {LabelContext, useContextProps} from 'react-aria-components'; const MyCustomLabel = React.forwardRef( (props: LabelProps, ref: React.ForwardedRef<HTMLLabelElement>) => { // Merge the local props and ref with the ones provided via context. [props, ref] = useContextProps(props, ref, LabelContext); // ... your existing Label component return <label {...props} ref={ref} />; } ); import type {LabelProps} from 'react-aria-components'; import { LabelContext, useContextProps } from 'react-aria-components'; const MyCustomLabel = React.forwardRef( ( props: LabelProps, ref: React.ForwardedRef<HTMLLabelElement> ) => { // Merge the local props and ref with the ones provided via context. [props, ref] = useContextProps( props, ref, LabelContext ); // ... your existing Label component return <label {...props} ref={ref} />; } ); import type {LabelProps} from 'react-aria-components'; import { LabelContext, useContextProps } from 'react-aria-components'; const MyCustomLabel = React.forwardRef( ( props: LabelProps, ref: React.ForwardedRef< HTMLLabelElement > ) => { // Merge the local props and ref with the ones provided via context. [props, ref] = useContextProps( props, ref, LabelContext ); // ... your existing Label component return ( <label {...props} ref={ref} /> ); } ); Since it consumes from `LabelContext`, `MyCustomLabel` can be used within any React Aria component instead of the built-in `Label`. <TextField> <MyCustomLabel>Name</MyCustomLabel> <Input /> </TextField> <TextField> <MyCustomLabel>Name</MyCustomLabel> <Input /> </TextField> <TextField> <MyCustomLabel> Name </MyCustomLabel> <Input /> </TextField> #### useSlottedContext# To consume a context without merging with existing props, use the `useSlottedContext` hook. This works like React's `useContext`, and also accepts an optional slot argument to identify which slot name to consume. import {useSlottedContext} from 'react-aria-components'; // Consume the un-slotted value. let buttonContext = useSlottedContext(ButtonContext); // Consume the value for a specific slot name. let incrementButtonContext = useSlottedContext(ButtonContext, 'increment'); import {useSlottedContext} from 'react-aria-components'; // Consume the un-slotted value. let buttonContext = useSlottedContext(ButtonContext); // Consume the value for a specific slot name. let incrementButtonContext = useSlottedContext( ButtonContext, 'increment' ); import {useSlottedContext} from 'react-aria-components'; // Consume the un-slotted value. let buttonContext = useSlottedContext( ButtonContext ); // Consume the value for a specific slot name. let incrementButtonContext = useSlottedContext( ButtonContext, 'increment' ); ### Accessing state# Most React Aria Components compose other standalone components in their children to build larger patterns. However, some components are made up of more tightly coupled children. For example, Calendar includes children such as `CalendarGrid` and `CalendarCell` that cannot be used standalone, and must appear within a `Calendar` or `RangeCalendar`. These components access the state from their parent via context. You can access the state from a parent component via the same contexts in order to build your own custom children. This example shows a `CalendarValue` component that displays the currently selected date from a calendar as a formatted string. import {CalendarStateContext} from 'react-aria-components'; import {useDateFormatter} from 'react-aria'; import {getLocalTimeZone} from '@internationalized/date'; function CalendarValue() { let state = React.useContext(CalendarStateContext)!; let date = state.value?.toDate(getLocalTimeZone()); let {format} = useDateFormatter(); let formatted = date ? format(date) : 'None'; return `Selected date: ${formatted}`; } import {CalendarStateContext} from 'react-aria-components'; import {useDateFormatter} from 'react-aria'; import {getLocalTimeZone} from '@internationalized/date'; function CalendarValue() { let state = React.useContext(CalendarStateContext)!; let date = state.value?.toDate(getLocalTimeZone()); let {format} = useDateFormatter(); let formatted = date ? format(date) : 'None'; return `Selected date: ${formatted}`; } import {CalendarStateContext} from 'react-aria-components'; import {useDateFormatter} from 'react-aria'; import {getLocalTimeZone} from '@internationalized/date'; function CalendarValue() { let state = React .useContext( CalendarStateContext )!; let date = state.value ?.toDate( getLocalTimeZone() ); let { format } = useDateFormatter(); let formatted = date ? format(date) : 'None'; return `Selected date: ${formatted}`; } This enables a `<CalendarValue>` to be placed inside a `<Calendar>` to display the current value. <Calendar> {/* ... */} <CalendarValue /></Calendar> <Calendar> {/* ... */} <CalendarValue /></Calendar> <Calendar> {/* ... */} <CalendarValue /></Calendar> The state interfaces and their associated contexts supported by each component are listed in the Advanced Customization section of their documentation. ## Hooks# * * * If you need to customize things even further, such as overriding behavior, intercepting events, or customizing DOM structure, you can drop down to the lower level Hook-based API. Hooks only provide behavior and leave all rendering to you. This gives you more control and flexibility, but requires additional glue code to set up. React Aria Components and Hooks can be used together, allowing you to mix and match depending on the level of customization you require. We recommend starting with the component API by default, and only dropping down to hooks when you need to customize something that the component API does not allow. Some potential use cases for Hooks are: * Overriding which DOM element a component renders * Rendering a subset of the children (e.g. virtualized scrolling) * Intercepting a DOM event to apply conditional logic * Overriding internal state management behavior * Customizing overlay positioning * Removing unused features to reduce bundle size ### Setup# As described above, each React Aria component exports a corresponding context. You can build a custom implementation of a component using Hooks by consuming from the relevant context with `useContextProps` . This example shows how a custom checkbox could be set up using `CheckboxContext` from `react-aria-components` and the useCheckbox hook from `react-aria`. import type {CheckboxProps} from 'react-aria-components'; import {CheckboxContext, useContextProps} from 'react-aria-components'; import {useToggleState} from 'react-stately'; import {useCheckbox} from 'react-aria'; const MyCheckbox = React.forwardRef( (props: CheckboxProps, ref: React.ForwardedRef<HTMLInputElement>) => { // Merge the local props and ref with the ones provided via context. [props, ref] = useContextProps(props, ref, CheckboxContext); // Follow the hook docs and implement your customizations... let state = useToggleState(props); let { inputProps } = useCheckbox(props, state, ref); return <input {...inputProps} ref={ref} />; } ); import type {CheckboxProps} from 'react-aria-components'; import { CheckboxContext, useContextProps } from 'react-aria-components'; import {useToggleState} from 'react-stately'; import {useCheckbox} from 'react-aria'; const MyCheckbox = React.forwardRef( ( props: CheckboxProps, ref: React.ForwardedRef<HTMLInputElement> ) => { // Merge the local props and ref with the ones provided via context. [props, ref] = useContextProps( props, ref, CheckboxContext ); // Follow the hook docs and implement your customizations... let state = useToggleState(props); let { inputProps } = useCheckbox(props, state, ref); return <input {...inputProps} ref={ref} />; } ); import type {CheckboxProps} from 'react-aria-components'; import { CheckboxContext, useContextProps } from 'react-aria-components'; import {useToggleState} from 'react-stately'; import {useCheckbox} from 'react-aria'; const MyCheckbox = React .forwardRef( ( props: CheckboxProps, ref: React.ForwardedRef< HTMLInputElement > ) => { // Merge the local props and ref with the ones provided via context. [props, ref] = useContextProps( props, ref, CheckboxContext ); // Follow the hook docs and implement your customizations... let state = useToggleState( props ); let { inputProps } = useCheckbox( props, state, ref ); return ( <input {...inputProps} ref={ref} /> ); } ); Since `MyCheckbox` consumes from `CheckboxContext` it can be used within other React Aria Components in place of the built-in `Checkbox`, such as within a Table or GridList. This lets you provide a custom checkbox implementation without rewriting all other React Aria Components you might use it in. <GridList> <GridListItem> <MyCheckbox slot="selection" /> {/* ... */} </GridListItem> </GridList> <GridList> <GridListItem> <MyCheckbox slot="selection" /> {/* ... */} </GridListItem> </GridList> <GridList> <GridListItem> <MyCheckbox slot="selection" /> {/* ... */} </GridListItem> </GridList> ### Reusing children# You can also provide values for React Aria Components from a Hook-based implementation. This allows you to customize the parent component of a larger pattern, while reusing the existing implementations of the child elements from React Aria Components. This example shows how a custom number field could be set up. First, follow the docs for useNumberField, and then use Provider to send values returned by the hook to each of the child elements via their corresponding contexts. import type {NumberFieldProps} from 'react-aria-components'; import {ButtonContext, GroupContext, InputContext, LabelContext, Provider} from 'react-aria-components'; import {useNumberFieldState} from 'react-stately'; import {useLocale, useNumberField} from 'react-aria'; function CustomNumberField(props: NumberFieldProps) { // Follow the hook docs... let { locale } = useLocale(); let state = useNumberFieldState({ ...props, locale }); let ref = useRef<HTMLInputElement>(null); let { labelProps, groupProps, inputProps, incrementButtonProps, decrementButtonProps } = useNumberField(props, state, ref); // Provide values for the child components via context. return ( <Provider values={[ [GroupContext, groupProps], [InputContext, { ...inputProps, ref }], [LabelContext, labelProps], [ButtonContext, { slots: { increment: incrementButtonProps, decrement: decrementButtonProps } }] ]} > {props.children} </Provider> ); } import type {NumberFieldProps} from 'react-aria-components'; import { ButtonContext, GroupContext, InputContext, LabelContext, Provider } from 'react-aria-components'; import {useNumberFieldState} from 'react-stately'; import {useLocale, useNumberField} from 'react-aria'; function CustomNumberField(props: NumberFieldProps) { // Follow the hook docs... let { locale } = useLocale(); let state = useNumberFieldState({ ...props, locale }); let ref = useRef<HTMLInputElement>(null); let { labelProps, groupProps, inputProps, incrementButtonProps, decrementButtonProps } = useNumberField(props, state, ref); // Provide values for the child components via context. return ( <Provider values={[ [GroupContext, groupProps], [InputContext, { ...inputProps, ref }], [LabelContext, labelProps], [ButtonContext, { slots: { increment: incrementButtonProps, decrement: decrementButtonProps } }] ]} > {props.children} </Provider> ); } import type {NumberFieldProps} from 'react-aria-components'; import { ButtonContext, GroupContext, InputContext, LabelContext, Provider } from 'react-aria-components'; import {useNumberFieldState} from 'react-stately'; import { useLocale, useNumberField } from 'react-aria'; function CustomNumberField( props: NumberFieldProps ) { // Follow the hook docs... let { locale } = useLocale(); let state = useNumberFieldState({ ...props, locale }); let ref = useRef< HTMLInputElement >(null); let { labelProps, groupProps, inputProps, incrementButtonProps, decrementButtonProps } = useNumberField( props, state, ref ); // Provide values for the child components via context. return ( <Provider values={[ [ GroupContext, groupProps ], [InputContext, { ...inputProps, ref }], [ LabelContext, labelProps ], [ButtonContext, { slots: { increment: incrementButtonProps, decrement: decrementButtonProps } }] ]} > {props.children} </Provider> ); } Because `CustomNumberField` provides values for the `Group`, `Input`, `Label`, and `Button` components via context, the implementations from React Aria Components can be reused. <CustomNumberField> <Label>Width</Label> <Group> <Input /> <Button slot="increment">+</Button> <Button slot="decrement">-</Button> </Group> </CustomNumberField> <CustomNumberField> <Label>Width</Label> <Group> <Input /> <Button slot="increment">+</Button> <Button slot="decrement">-</Button> </Group> </CustomNumberField> <CustomNumberField> <Label>Width</Label> <Group> <Input /> <Button slot="increment"> + </Button> <Button slot="decrement"> - </Button> </Group> </CustomNumberField> ### Examples# The contexts provided and consumed by each component, along with the corresponding hooks, are listed in the Advanced Customization section of their documentation. The corresponding hook docs cover the implementation and APIs of each component in detail. The source code of React Aria Components can also be a good resource when building a custom implementation of a component. This may help you understand how all of the hooks and contexts fit together. You can also start by copy and pasting the source for a component from React Aria Components into your project, using this as a starting point to make your customizations. --- ## Page: https://react-spectrum.adobe.com/react-aria/Button.html # Button A button allows a user to perform an action, with mouse, touch, and keyboard interactions. <table><tbody><tr><th>install</th><td><code>yarn add react-aria-components</code></td></tr><tr><th>version</th><td>1.10.1</td></tr><tr><th>usage</th><td><code><span>import</span> {Button} <span>from</span> <span>'react-aria-components'</span></code></td></tr></tbody></table> View ARIA pattern W3C View repository GitHub View package NPM ## Example# * * * import {Button} from 'react-aria-components'; <Button onPress={() => alert('Hello world!')}>Press me</Button> import {Button} from 'react-aria-components'; <Button onPress={() => alert('Hello world!')}> Press me </Button> import {Button} from 'react-aria-components'; <Button onPress={() => alert( 'Hello world!' )} > Press me </Button> Press me Show CSS @import "@react-aria/example-theme"; .react-aria-Button { color: var(--text-color); background: var(--button-background); border: 1px solid var(--border-color); border-radius: 4px; appearance: none; vertical-align: middle; font-size: 1rem; text-align: center; margin: 0; outline: none; padding: 6px 10px; text-decoration: none; &[data-pressed] { box-shadow: inset 0 1px 2px rgb(0 0 0 / 0.1); background: var(--button-background-pressed); border-color: var(--border-color-pressed); } &[data-focus-visible] { outline: 2px solid var(--focus-ring-color); outline-offset: -1px; } } @import "@react-aria/example-theme"; .react-aria-Button { color: var(--text-color); background: var(--button-background); border: 1px solid var(--border-color); border-radius: 4px; appearance: none; vertical-align: middle; font-size: 1rem; text-align: center; margin: 0; outline: none; padding: 6px 10px; text-decoration: none; &[data-pressed] { box-shadow: inset 0 1px 2px rgb(0 0 0 / 0.1); background: var(--button-background-pressed); border-color: var(--border-color-pressed); } &[data-focus-visible] { outline: 2px solid var(--focus-ring-color); outline-offset: -1px; } } @import "@react-aria/example-theme"; .react-aria-Button { color: var(--text-color); background: var(--button-background); border: 1px solid var(--border-color); border-radius: 4px; appearance: none; vertical-align: middle; font-size: 1rem; text-align: center; margin: 0; outline: none; padding: 6px 10px; text-decoration: none; &[data-pressed] { box-shadow: inset 0 1px 2px rgb(0 0 0 / 0.1); background: var(--button-background-pressed); border-color: var(--border-color-pressed); } &[data-focus-visible] { outline: 2px solid var(--focus-ring-color); outline-offset: -1px; } } ## Features# * * * On the surface, building a custom styled button seems simple. However, there are many cross browser inconsistencies in interactions and accessibility features to consider. `Button` handles all of these interactions for you, so you can focus on the styling. * **Styleable** – Hover, press, and keyboard focus states are provided for easy styling. These states only apply when interacting with an appropriate input device, unlike CSS pseudo classes. * **Accessible** – Uses a native `<button>` element under the hood, with support for the Space and Enter keys. * **Cross-browser** – Mouse, touch, keyboard, and focus interactions are normalized to ensure consistency across browsers and devices. Read our blog post about the complexities of building buttons that work well across devices and interaction methods to learn more. ## Anatomy# * * * Buttons consist of a clickable area usually containing a textual label or an icon that users can click to perform an action. In addition, keyboard users may activate buttons using the Space or Enter keys. If a visual label is not provided (e.g. an icon only button), then an `aria-label` or `aria-labelledby` prop must be passed to identify the button to assistive technology. ## Examples# * * * Ripple Button A button with an animated ripple effect styled with Tailwind CSS. ## Starter kits# * * * To help kick-start your project, we offer starter kits that include example implementations of all React Aria components with various styling solutions. All components are fully styled, including support for dark mode, high contrast mode, and all UI states. Each starter comes with a pre-configured Storybook that you can experiment with, or use as a starting point for your own component library. Vanilla CSS Download ZIP Preview Tailwind CSS Download ZIP Preview ## Events# * * * `Button` supports user interactions via mouse, keyboard, and touch. You can handle all of these via the `onPress` prop. This is similar to the standard `onClick` event, but normalized to support all interaction methods equally. In addition, the `onPressStart`, `onPressEnd`, and `onPressChange` events are fired as the user interacts with the button. Each of these handlers receives a `PressEvent` , which exposes information about the target and the type of event that triggered the interaction. See usePress for more details. function Example() { let [pointerType, setPointerType] = React.useState(''); return ( <> <Button onPressStart={(e) => setPointerType(e.pointerType)} onPressEnd={() => setPointerType('')} > Press me </Button> <p> {pointerType ? `You are pressing the button with a ${pointerType}!` : 'Ready to be pressed.'} </p> </> ); } function Example() { let [pointerType, setPointerType] = React.useState(''); return ( <> <Button onPressStart={(e) => setPointerType(e.pointerType)} onPressEnd={() => setPointerType('')} > Press me </Button> <p> {pointerType ? `You are pressing the button with a ${pointerType}!` : 'Ready to be pressed.'} </p> </> ); } function Example() { let [ pointerType, setPointerType ] = React.useState(''); return ( <> <Button onPressStart={(e) => setPointerType( e.pointerType )} onPressEnd={() => setPointerType( '' )} > Press me </Button> <p> {pointerType ? `You are pressing the button with a ${pointerType}!` : 'Ready to be pressed.'} </p> </> ); } Press me Ready to be pressed. ## Disabled# * * * A `Button` can be disabled using the `isDisabled` prop. <Button isDisabled>Pin</Button> <Button isDisabled>Pin</Button> <Button isDisabled> Pin </Button> Pin Show CSS .react-aria-Button { &[data-disabled]{ border-color: var(--border-color-disabled); color: var(--text-color-disabled); } } .react-aria-Button { &[data-disabled]{ border-color: var(--border-color-disabled); color: var(--text-color-disabled); } } .react-aria-Button { &[data-disabled]{ border-color: var(--border-color-disabled); color: var(--text-color-disabled); } } ## Pending# * * * A `Button` can be put into a pending state using the `isPending` prop. This is useful when an action takes a long time to complete, and you want to provide feedback to the user that the action is in progress. The pending state announces the state change to assistive technologies and disables interactions with the exception of focus. A ProgressBar component is required to show the pending state correctly. Make sure to internationalize the `aria-label` you pass to the ProgressBar component. import {useState} from 'react'; function PendingButton(props) { let [isPending, setPending] = useState(false); let handlePress = (e) => { setPending(true); setTimeout(() => { setPending(false); }, 5000); }; return ( <Button {...props} isPending={isPending} onPress={handlePress}> {({isPending}) => ( <> {!isPending && <span>Save</span>} {isPending && ( // See below <MyProgressCircle aria-label="Saving..." isIndeterminate /> )} </> )} </Button> ); } import {useState} from 'react'; function PendingButton(props) { let [isPending, setPending] = useState(false); let handlePress = (e) => { setPending(true); setTimeout(() => { setPending(false); }, 5000); }; return ( <Button {...props} isPending={isPending} onPress={handlePress} > {({ isPending }) => ( <> {!isPending && <span>Save</span>} {isPending && ( // See below <MyProgressCircle aria-label="Saving..." isIndeterminate /> )} </> )} </Button> ); } import {useState} from 'react'; function PendingButton( props ) { let [ isPending, setPending ] = useState(false); let handlePress = ( e ) => { setPending(true); setTimeout(() => { setPending(false); }, 5000); }; return ( <Button {...props} isPending={isPending} onPress={handlePress} > {( { isPending } ) => ( <> {!isPending && ( <span> Save </span> )} {isPending && ( // See below <MyProgressCircle aria-label="Saving..." isIndeterminate /> )} </> )} </Button> ); } Save MyProgressCircle import {ProgressBar} from 'react-aria-components'; import type {ProgressBarProps} from 'react-aria-components'; function MyProgressCircle(props: ProgressBarProps) { return ( <ProgressBar {...props}> <svg width="16" height="16" viewBox="0 0 24 24" style={{ display: 'block' }} > <path fill="currentColor" d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z" opacity=".25" /> <path fill="currentColor" d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z" > <animateTransform attributeName="transform" type="rotate" dur="0.75s" values="0 12 12;360 12 12" repeatCount="indefinite" /> </path> </svg> </ProgressBar> ); } import {ProgressBar} from 'react-aria-components'; import type {ProgressBarProps} from 'react-aria-components'; function MyProgressCircle(props: ProgressBarProps) { return ( <ProgressBar {...props}> <svg width="16" height="16" viewBox="0 0 24 24" style={{ display: 'block' }} > <path fill="currentColor" d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z" opacity=".25" /> <path fill="currentColor" d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z" > <animateTransform attributeName="transform" type="rotate" dur="0.75s" values="0 12 12;360 12 12" repeatCount="indefinite" /> </path> </svg> </ProgressBar> ); } import {ProgressBar} from 'react-aria-components'; import type {ProgressBarProps} from 'react-aria-components'; function MyProgressCircle( props: ProgressBarProps ) { return ( <ProgressBar {...props} > <svg width="16" height="16" viewBox="0 0 24 24" style={{ display: 'block' }} > <path fill="currentColor" d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z" opacity=".25" /> <path fill="currentColor" d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z" > <animateTransform attributeName="transform" type="rotate" dur="0.75s" values="0 12 12;360 12 12" repeatCount="indefinite" /> </path> </svg> </ProgressBar> ); } ### Accessibility# **Note:** The `ProgressBar` must be in the accessibility tree as soon as the button becomes pending, even if it is not visible. For example, if you'd like to delay showing a spinner until a minimum amount of time passes, you could use `opacity: 0` to hide it so it is still available to screen readers. Do not use `visibility: hidden` or `display: none` since these remove the element from the accessibility tree. Additionally, you may choose to keep the button's contents in the DOM while the button is pending, e.g. to preserve the button's layout. If you hide the contents with `visibility: hidden`, the accessibility label for the button will only include the ProgressBar, so it should have a descriptive `aria-label` (e.g. "Saving"). You can also choose to keep the button's contents in the accessibility tree by using `opacity: 0`, in which case the `ProgressBar`'s label will be combined with the contents (e.g. "Save, pending"). Try the above example and the one below with a screen reader to see the difference in behavior. Show example function PendingDelayed(props) { let [isPending, setPending] = useState(false); let handlePress = (e) => { setPending(true); setTimeout(() => { setPending(false); }, 5000); }; return ( <Button {...props} isPending={isPending} onPress={handlePress} style={{ position: 'relative' }} > {({ isPending }) => ( <> <span className={isPending ? 'pending' : undefined}>Save</span> {isPending && ( <MyProgressCircle aria-label="in progress" isIndeterminate className="spinner" /> )} </> )} </Button> ); } function PendingDelayed(props) { let [isPending, setPending] = useState(false); let handlePress = (e) => { setPending(true); setTimeout(() => { setPending(false); }, 5000); }; return ( <Button {...props} isPending={isPending} onPress={handlePress} style={{ position: 'relative' }} > {({ isPending }) => ( <> <span className={isPending ? 'pending' : undefined} > Save </span> {isPending && ( <MyProgressCircle aria-label="in progress" isIndeterminate className="spinner" /> )} </> )} </Button> ); } function PendingDelayed( props ) { let [ isPending, setPending ] = useState(false); let handlePress = ( e ) => { setPending(true); setTimeout(() => { setPending(false); }, 5000); }; return ( <Button {...props} isPending={isPending} onPress={handlePress} style={{ position: 'relative' }} > {( { isPending } ) => ( <> <span className={isPending ? 'pending' : undefined} > Save </span> {isPending && ( <MyProgressCircle aria-label="in progress" isIndeterminate className="spinner" /> )} </> )} </Button> ); } Save @keyframes toggle { from { opacity: 0; } to { opacity: 1; } } .spinner { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); animation: toggle 1s steps(1); opacity: 1; } .pending { animation: toggle 1s reverse steps(1, jump-start); opacity: 0; } @keyframes toggle { from { opacity: 0; } to { opacity: 1; } } .spinner { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); animation: toggle 1s steps(1); opacity: 1; } .pending { animation: toggle 1s reverse steps(1, jump-start); opacity: 0; } @keyframes toggle { from { opacity: 0; } to { opacity: 1; } } .spinner { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); animation: toggle 1s steps(1); opacity: 1; } .pending { animation: toggle 1s reverse steps(1, jump-start); opacity: 0; } ## Link buttons# * * * The `Button` component always represents a button semantically. To create a link that visually looks like a button, use the Link component instead. You can reuse the same styles you apply to the `Button` component on the `Link`. import {Link} from 'react-aria-components'; <Link className="react-aria-Button" href="https://adobe.com/" target="_blank"> Adobe </Link> import {Link} from 'react-aria-components'; <Link className="react-aria-Button" href="https://adobe.com/" target="_blank" > Adobe </Link> import {Link} from 'react-aria-components'; <Link className="react-aria-Button" href="https://adobe.com/" target="_blank" > Adobe </Link> Adobe ## Props# * * * | Name | Type | Default | Description | | --- | --- | --- | --- | | `form` | `string` | — | The `<form>` element to associate the button with. The value of this attribute must be the id of a `<form>` in the same document. | | `formAction` | `string` | — | The URL that processes the information submitted by the button. Overrides the action attribute of the button's form owner. | | `formEncType` | `string` | — | Indicates how to encode the form data that is submitted. | | `formMethod` | `string` | — | Indicates the HTTP method used to submit the form. | | `formNoValidate` | `boolean` | — | Indicates that the form is not to be validated when it is submitted. | | `formTarget` | `string` | — | Overrides the target attribute of the button's form owner. | | `name` | `string` | — | Submitted as a pair with the button's value as part of the form data. | | `value` | `string` | — | The value associated with the button's name when it's submitted with the form data. | | `isPending` | `boolean` | — | Whether the button is in a pending state. This disables press and hover events while retaining focusability, and announces the pending state to screen readers. | | `isDisabled` | `boolean` | — | Whether the button is disabled. | | `autoFocus` | `boolean` | — | Whether the element should receive focus on render. | | `type` | `'button' | 'submit' | 'reset'` | `'button'` | The behavior of the button when used in an HTML form. | | `children` | `ReactNode | ( (values: ButtonRenderProps & & { defaultChildren: ReactNode | | undefined } )) => ReactNode` | — | The children of the component. A function may be provided to alter the children based on component state. | | `className` | `string | ( (values: ButtonRenderProps & & { defaultClassName: string | | undefined } )) => string` | — | The CSS className for the element. A function may be provided to compute the class based on component state. | | `style` | `CSSProperties | ( (values: ButtonRenderProps & & { defaultStyle: CSSProperties } )) => CSSProperties | undefined` | — | The inline style for the element. A function may be provided to compute the style based on component state. | Events | Name | Type | Description | | --- | --- | --- | | `onPress` | `( (e: PressEvent )) => void` | Handler that is called when the press is released over the target. | | `onPressStart` | `( (e: PressEvent )) => void` | Handler that is called when a press interaction starts. | | `onPressEnd` | `( (e: PressEvent )) => void` | Handler that is called when a press interaction ends, either over the target or when the pointer leaves the target. | | `onPressChange` | `( (isPressed: boolean )) => void` | Handler that is called when the press state changes. | | `onPressUp` | `( (e: PressEvent )) => void` | Handler that is called when a press is released over the target, regardless of whether it started on the target or not. | | `onClick` | `( (e: MouseEvent<FocusableElement> )) => void` | **Not recommended – use `onPress` instead.** `onClick` is an alias for `onPress` provided for compatibility with other libraries. `onPress` provides additional event details for non-mouse interactions. | | `onFocus` | `( (e: FocusEvent<Target> )) => void` | Handler that is called when the element receives focus. | | `onBlur` | `( (e: FocusEvent<Target> )) => void` | Handler that is called when the element loses focus. | | `onFocusChange` | `( (isFocused: boolean )) => void` | Handler that is called when the element's focus status changes. | | `onKeyDown` | `( (e: KeyboardEvent )) => void` | Handler that is called when a key is pressed. | | `onKeyUp` | `( (e: KeyboardEvent )) => void` | Handler that is called when a key is released. | | `onHoverStart` | `( (e: HoverEvent )) => void` | Handler that is called when a hover interaction starts. | | `onHoverEnd` | `( (e: HoverEvent )) => void` | Handler that is called when a hover interaction ends. | | `onHoverChange` | `( (isHovering: boolean )) => void` | Handler that is called when the hover state changes. | Layout | Name | Type | Description | | --- | --- | --- | | `slot` | `string | null` | A slot name for the component. Slots allow the component to receive props from a parent component. An explicit `null` value indicates that the local props completely override all props received from a parent. | Accessibility | Name | Type | Description | | --- | --- | --- | | `id` | `string` | The element's unique identifier. See MDN. | | `excludeFromTabOrder` | `boolean` | Whether to exclude the element from the sequential tab order. If true, the element will not be focusable via the keyboard by tabbing. This should be avoided except in rare scenarios where an alternative means of accessing the element or its functionality via the keyboard is available. | | `preventFocusOnPress` | `boolean` | Whether to prevent focus from moving to the button when pressing it. Caution, this can make the button inaccessible and should only be used when alternative keyboard interaction is provided, such as ComboBox's MenuTrigger or a NumberField's increment/decrement control. | | `aria-expanded` | `boolean | 'true' | 'false'` | Indicates whether the element, or another grouping element it controls, is currently expanded or collapsed. | | `aria-haspopup` | `boolean | 'menu' | 'listbox' | 'tree' | 'grid' | 'dialog' | 'true' | 'false'` | Indicates the availability and type of interactive popup element, such as menu or dialog, that can be triggered by an element. | | `aria-controls` | `string` | Identifies the element (or elements) whose contents or presence are controlled by the current element. | | `aria-pressed` | `boolean | 'true' | 'false' | 'mixed'` | Indicates the current "pressed" state of toggle buttons. | | `aria-current` | `boolean | 'true' | 'false' | 'page' | 'step' | 'location' | 'date' | 'time'` | Indicates whether this element represents the current item within a container or set of related elements. | | `aria-label` | `string` | Defines a string value that labels the current element. | | `aria-labelledby` | `string` | Identifies the element (or elements) that labels the current element. | | `aria-describedby` | `string` | Identifies the element (or elements) that describes the object. | | `aria-details` | `string` | Identifies the element (or elements) that provide a detailed, extended description for the object. | ## Styling# * * * React Aria components can be styled in many ways, including using CSS classes, inline styles, utility classes (e.g. Tailwind), CSS-in-JS (e.g. Styled Components), etc. By default, all components include a builtin `className` attribute which can be targeted using CSS selectors. These follow the `react-aria-ComponentName` naming convention. .react-aria-Button { /* ... */ } .react-aria-Button { /* ... */ } .react-aria-Button { /* ... */ } A custom `className` can also be specified on any component. This overrides the default `className` provided by React Aria with your own. <Button className="my-button"> {/* ... */} </Button> <Button className="my-button"> {/* ... */} </Button> <Button className="my-button"> {/* ... */} </Button> In addition, some components support multiple UI states (e.g. focused, placeholder, readonly, etc.). React Aria components expose states using data attributes, which you can target in CSS selectors. For example: .react-aria-Button[data-pressed] { /* ... */ } .react-aria-Button[data-pressed] { /* ... */ } .react-aria-Button[data-pressed] { /* ... */ } The `className` and `style` props also accept functions which receive states for styling. This lets you dynamically determine the classes or styles to apply, which is useful when using utility CSS libraries like Tailwind. <Button className={({ isPressed }) => isPressed ? 'bg-gray-700' : 'bg-gray-600'} /> <Button className={({ isPressed }) => isPressed ? 'bg-gray-700' : 'bg-gray-600'} /> <Button className={( { isPressed } ) => isPressed ? 'bg-gray-700' : 'bg-gray-600'} /> Render props may also be used as children to alter what elements are rendered based on the current state. For example, you could render an extra element when the button is in a pressed state. <Button> {({isPressed}) => ( <> {isPressed && <PressHighlight />} Press me </> )} </Button> <Button> {({isPressed}) => ( <> {isPressed && <PressHighlight />} Press me </> )} </Button> <Button> {({ isPressed }) => ( <> {isPressed && ( <PressHighlight /> )} Press me </> )} </Button> The states, selectors, and render props for `Button` are documented below. | Name | CSS Selector | Description | | --- | --- | --- | | `isHovered` | `[data-hovered]` | Whether the button is currently hovered with a mouse. | | `isPressed` | `[data-pressed]` | Whether the button is currently in a pressed state. | | `isFocused` | `[data-focused]` | Whether the button is focused, either via a mouse or keyboard. | | `isFocusVisible` | `[data-focus-visible]` | Whether the button is keyboard focused. | | `isDisabled` | `[data-disabled]` | Whether the button is disabled. | | `isPending` | `[data-pending]` | Whether the button is currently in a pending state. | ## Advanced customization# * * * ### Contexts# All React Aria Components export a corresponding context that can be used to send props to them from a parent element. This enables you to build your own compositional APIs similar to those found in React Aria Components itself. You can send any prop or ref via context that you could pass to the corresponding component. The local props and ref on the component are merged with the ones passed via context, with the local props taking precedence (following the rules documented in mergeProps). | Component | Context | Props | Ref | | --- | --- | --- | --- | | `Button` | `ButtonContext` | ` ButtonProps ` | `HTMLButtonElement` | This example shows a `ButtonGroup` component that renders a group of buttons. The entire group can be marked as disabled via the `isDisabled` prop, which is passed to all child buttons via the `ButtonContext` provider. import {ButtonContext} from 'react-aria-components'; interface ButtonGroupProps { children?: React.ReactNode, isDisabled?: boolean } function ButtonGroup({children, isDisabled}: ButtonGroupProps) { return ( <div style={{display: 'flex', gap: 8}}> <ButtonContext.Provider value={{isDisabled}}> {children} </ButtonContext.Provider> </div> ); } <ButtonGroup isDisabled> <Button>Save</Button> <Button>Publish</Button> </ButtonGroup> import {ButtonContext} from 'react-aria-components'; interface ButtonGroupProps { children?: React.ReactNode; isDisabled?: boolean; } function ButtonGroup( { children, isDisabled }: ButtonGroupProps ) { return ( <div style={{ display: 'flex', gap: 8 }}> <ButtonContext.Provider value={{ isDisabled }}> {children} </ButtonContext.Provider> </div> ); } <ButtonGroup isDisabled> <Button>Save</Button> <Button>Publish</Button> </ButtonGroup> import {ButtonContext} from 'react-aria-components'; interface ButtonGroupProps { children?: React.ReactNode; isDisabled?: boolean; } function ButtonGroup( { children, isDisabled }: ButtonGroupProps ) { return ( <div style={{ display: 'flex', gap: 8 }} > <ButtonContext.Provider value={{ isDisabled }} > {children} </ButtonContext.Provider> </div> ); } <ButtonGroup isDisabled > <Button>Save</Button> <Button> Publish </Button> </ButtonGroup> SavePublish ### Hooks# If you need to customize things further, such as intercepting events or customizing DOM elements, you can drop down to the lower level Hook-based API. Consume from `ButtonContext` in your component with `useContextProps` to make it compatible with other React Aria Components. See useButton for more details. This example uses Framer Motion to create an `AnimatedButton` component that animates based on the `isPressed` state provided by `useButton`. It can be used standalone or as a part of any React Aria component. import type {ButtonProps} from 'react-aria-components'; import {ButtonContext, useContextProps} from 'react-aria-components'; import {useButton} from 'react-aria'; import {motion} from 'framer-motion'; const AnimatedButton = React.forwardRef( (props: ButtonProps, ref: React.ForwardedRef<HTMLButtonElement>) => { // Merge the local props and ref with the ones provided via context. [props, ref] = useContextProps(props, ref, ButtonContext); let { buttonProps, isPressed } = useButton(props, ref); return ( <motion.button {...buttonProps} ref={ref} animate={{ scale: isPressed ? 0.9 : 1 }} > {props.children} </motion.button> ); } ); import type {ButtonProps} from 'react-aria-components'; import { ButtonContext, useContextProps } from 'react-aria-components'; import {useButton} from 'react-aria'; import {motion} from 'framer-motion'; const AnimatedButton = React.forwardRef( ( props: ButtonProps, ref: React.ForwardedRef<HTMLButtonElement> ) => { // Merge the local props and ref with the ones provided via context. [props, ref] = useContextProps( props, ref, ButtonContext ); let { buttonProps, isPressed } = useButton(props, ref); return ( <motion.button {...buttonProps} ref={ref} animate={{ scale: isPressed ? 0.9 : 1 }} > {props.children} </motion.button> ); } ); import type {ButtonProps} from 'react-aria-components'; import { ButtonContext, useContextProps } from 'react-aria-components'; import {useButton} from 'react-aria'; import {motion} from 'framer-motion'; const AnimatedButton = React.forwardRef( ( props: ButtonProps, ref: React.ForwardedRef< HTMLButtonElement > ) => { // Merge the local props and ref with the ones provided via context. [props, ref] = useContextProps( props, ref, ButtonContext ); let { buttonProps, isPressed } = useButton( props, ref ); return ( <motion.button {...buttonProps} ref={ref} animate={{ scale: isPressed ? 0.9 : 1 }} > {props .children} </motion.button> ); } ); --- ## Page: https://react-spectrum.adobe.com/react-aria/FileTrigger.html # FileTrigger A FileTrigger allows a user to access the file system with any pressable React Aria or React Spectrum component, or custom components built with usePress. <table><tbody><tr><th>install</th><td><code>yarn add react-aria-components</code></td></tr><tr><th>version</th><td>1.10.1</td></tr><tr><th>usage</th><td><code><span>import</span> {FileTrigger} <span>from</span> <span>'react-aria-components'</span></code></td></tr></tbody></table> View repository GitHub View package NPM ## Example# * * * import {FileTrigger, Button} from 'react-aria-components'; function Example(){ let [file, setFile] = React.useState(null); return ( <> <FileTrigger onSelect={(e) => { let files = e ? Array.from(e) : []; let filenames = files.map((file) => file.name); setFile(filenames); }}> <Button>Select a file</Button> </FileTrigger> {file && file} </> ) } import {FileTrigger, Button} from 'react-aria-components'; function Example(){ let [file, setFile] = React.useState(null); return ( <> <FileTrigger onSelect={(e) => { let files = e ? Array.from(e) : []; let filenames = files.map((file) => file.name); setFile(filenames); }}> <Button>Select a file</Button> </FileTrigger> {file && file} </> ) } import { Button, FileTrigger } from 'react-aria-components'; function Example() { let [file, setFile] = React.useState(null); return ( <> <FileTrigger onSelect={( e ) => { let files = e ? Array.from( e ) : []; let filenames = files.map(( file ) => file.name ); setFile( filenames ); }} > <Button> Select a file </Button> </FileTrigger> {file && file} </> ); } Select a file ## Features# * * * A file input can be created with an `<input type=“file”>` element, but this supports limited styling options and may not integrate well with the overall design of a website or application. To overcome this, `FileTrigger` extends the functionality of the standard file input element by working with a pressable child such as a `Button` to create accessible file inputs that can be styled as needed. * **Customizable** – Works with any pressable React Aria or React Spectrum component, and custom components built with usePress. ## Anatomy# * * * A `FileTrigger` wraps around a pressable child such as a button, and includes a visually hidden input element that allows the user to select files from their device. import {FileTrigger, Button} from 'react-aria-components'; <FileTrigger> <Button /> </FileTrigger> import {FileTrigger, Button} from 'react-aria-components'; <FileTrigger> <Button /> </FileTrigger> import { Button, FileTrigger } from 'react-aria-components'; <FileTrigger> <Button /> </FileTrigger> If a visual label is not provided on the pressable child, then an `aria-label` or `aria-labelledby` prop must be passed to identify the file trigger to assistive technology. ### Composed Components# A `FileTrigger` can use the following components, which may also be used standalone or reused in other components. Button A button allows a user to perform an action with a mouse, touch, or keyboard. ## Accepted file types# * * * By default, the file trigger will accept any file type. To support only certain file types, pass an array of the mime type of files via the `acceptedFileTypes` prop. <FileTrigger acceptedFileTypes={['image/png']}> <Button>Select files</Button> </FileTrigger> <FileTrigger acceptedFileTypes={['image/png']}> <Button>Select files</Button> </FileTrigger> <FileTrigger acceptedFileTypes={[ 'image/png' ]} > <Button> Select files </Button> </FileTrigger> Select files ## Multiple files# * * * A file trigger can accept multiple files by passsing the `allowsMultiple` property. <FileTrigger allowsMultiple> <Button>Upload your files</Button> </FileTrigger> <FileTrigger allowsMultiple> <Button>Upload your files</Button> </FileTrigger> <FileTrigger allowsMultiple > <Button> Upload your files </Button> </FileTrigger> Upload your files ## Directory selection# * * * To enable selecting directories instead of files, use the `acceptDirectory` property. This reflects the webkitdirectory HTML attribute and allows users to select directories and their contents. Please note that support for this feature varies from browser to browser. function Example() { let [files, setFiles] = React.useState([]); return ( <> <FileTrigger acceptDirectory onSelect={(e) => { if (e) { let fileList = [...e].map((file) => file.webkitRelativePath !== '' ? file.webkitRelativePath : file.name ); setFiles(fileList); } }} > <Button>Upload</Button> </FileTrigger> {files && ( <ul> {files.map((file, index) => <li key={index}>{file}</li>)} </ul> )} </> ); } function Example() { let [files, setFiles] = React.useState([]); return ( <> <FileTrigger acceptDirectory onSelect={(e) => { if (e) { let fileList = [...e].map((file) => file.webkitRelativePath !== '' ? file.webkitRelativePath : file.name ); setFiles(fileList); } }} > <Button>Upload</Button> </FileTrigger> {files && ( <ul> {files.map((file, index) => ( <li key={index}>{file}</li> ))} </ul> )} </> ); } function Example() { let [files, setFiles] = React.useState([]); return ( <> <FileTrigger acceptDirectory onSelect={( e ) => { if (e) { let fileList = [...e].map( (file) => file .webkitRelativePath !== '' ? file .webkitRelativePath : file .name ); setFiles( fileList ); } }} > <Button> Upload </Button> </FileTrigger> {files && ( <ul> {files.map(( file, index ) => ( <li key={index} > {file} </li> ))} </ul> )} </> ); } Upload ## Media capture# * * * To specify the media capture mechanism to capture media on the spot, pass `user` for the user-facing camera or `environment` for the outward-facing camera via the `defaultCamera` prop. This behavior only works on mobile devices. On desktop devices, it will open the file system like normal. Read more about capture. <FileTrigger defaultCamera="environment"> <Button>Open Camera</Button> </FileTrigger> <FileTrigger defaultCamera="environment"> <Button>Open Camera</Button> </FileTrigger> <FileTrigger defaultCamera="environment"> <Button> Open Camera </Button> </FileTrigger> Open Camera ## Props# * * * | Name | Type | Description | | --- | --- | --- | | `acceptedFileTypes` | `Array<string>` | Specifies what mime type of files are allowed. | | `allowsMultiple` | `boolean` | Whether multiple files can be selected. | | `defaultCamera` | `'user' | 'environment'` | Specifies the use of a media capture mechanism to capture the media on the spot. | | `children` | `ReactNode` | The children of the component. | | `acceptDirectory` | `boolean` | Enables the selection of directories instead of individual files. | Events | Name | Type | Description | | --- | --- | --- | | `onSelect` | `( (files: FileList | | null )) => void` | Handler when a user selects a file. | ## Styling# * * * ### FileTrigger# The `FileTrigger` component does not render any element of its own so it does not support styling. ### Button# A Button can be targeted with the `.react-aria-Button` CSS selector, or by overriding with a custom `className`. It supports the following states: | Name | CSS Selector | Description | | --- | --- | --- | | `isHovered` | `[data-hovered]` | Whether the button is currently hovered with a mouse. | | `isPressed` | `[data-pressed]` | Whether the button is currently in a pressed state. | | `isFocused` | `[data-focused]` | Whether the button is focused, either via a mouse or keyboard. | | `isFocusVisible` | `[data-focus-visible]` | Whether the button is keyboard focused. | | `isDisabled` | `[data-disabled]` | Whether the button is disabled. | | `isPending` | `[data-pending]` | Whether the button is currently in a pending state. | --- ## Page: https://react-spectrum.adobe.com/react-aria/ToggleButton.html # ToggleButton A toggle button allows a user to toggle a selection on or off, for example switching between two states or modes. <table><tbody><tr><th>install</th><td><code>yarn add react-aria-components</code></td></tr><tr><th>version</th><td>1.10.1</td></tr><tr><th>usage</th><td><code><span>import</span> {ToggleButton} <span>from</span> <span>'react-aria-components'</span></code></td></tr></tbody></table> View ARIA pattern W3C View repository GitHub View package NPM ## Example# * * * import {ToggleButton} from 'react-aria-components'; <ToggleButton>Pin</ToggleButton> import {ToggleButton} from 'react-aria-components'; <ToggleButton>Pin</ToggleButton> import {ToggleButton} from 'react-aria-components'; <ToggleButton> Pin </ToggleButton> Pin Show CSS @import "@react-aria/example-theme"; .react-aria-ToggleButton { color: var(--text-color); background: var(--button-background); border: 1px solid var(--border-color); forced-color-adjust: none; border-radius: 4px; appearance: none; vertical-align: middle; font-size: 1rem; text-align: center; margin: 0; outline: none; padding: 6px 10px; &[data-pressed] { box-shadow: inset 0 1px 2px rgb(0 0 0 / 0.1); background: var(--button-background-pressed); border-color: var(--border-color-pressed); } &[data-selected] { background: var(--highlight-background); border-color: var(--highlight-background); color: var(--highlight-foreground); &[data-pressed] { background: var(--highlight-background-pressed); border-color: var(--highlight-background-pressed); } } &[data-focus-visible] { outline: 2px solid var(--focus-ring-color); outline-offset: 2px; } } @import "@react-aria/example-theme"; .react-aria-ToggleButton { color: var(--text-color); background: var(--button-background); border: 1px solid var(--border-color); forced-color-adjust: none; border-radius: 4px; appearance: none; vertical-align: middle; font-size: 1rem; text-align: center; margin: 0; outline: none; padding: 6px 10px; &[data-pressed] { box-shadow: inset 0 1px 2px rgb(0 0 0 / 0.1); background: var(--button-background-pressed); border-color: var(--border-color-pressed); } &[data-selected] { background: var(--highlight-background); border-color: var(--highlight-background); color: var(--highlight-foreground); &[data-pressed] { background: var(--highlight-background-pressed); border-color: var(--highlight-background-pressed); } } &[data-focus-visible] { outline: 2px solid var(--focus-ring-color); outline-offset: 2px; } } @import "@react-aria/example-theme"; .react-aria-ToggleButton { color: var(--text-color); background: var(--button-background); border: 1px solid var(--border-color); forced-color-adjust: none; border-radius: 4px; appearance: none; vertical-align: middle; font-size: 1rem; text-align: center; margin: 0; outline: none; padding: 6px 10px; &[data-pressed] { box-shadow: inset 0 1px 2px rgb(0 0 0 / 0.1); background: var(--button-background-pressed); border-color: var(--border-color-pressed); } &[data-selected] { background: var(--highlight-background); border-color: var(--highlight-background); color: var(--highlight-foreground); &[data-pressed] { background: var(--highlight-background-pressed); border-color: var(--highlight-background-pressed); } } &[data-focus-visible] { outline: 2px solid var(--focus-ring-color); outline-offset: 2px; } } ## Features# * * * Toggle buttons are similar to action buttons, but support an additional selection state that is toggled when a user presses the button. There is no built-in HTML element that represents a toggle button, so React Aria implements it using ARIA attributes. * **Styleable** – Hover, press, keyboard focus, and selection states are provided for easy styling. These states only apply when interacting with an appropriate input device, unlike CSS pseudo classes. * **Accessible** – Uses a native `<button>` element with the `aria-pressed` attribute, and supports the Space and Enter keys to toggle the selection state. * **Cross-browser** – Mouse, touch, keyboard, and focus interactions are normalized to ensure consistency across browsers and devices. Read our blog post about the complexities of building buttons that work well across devices and interaction methods to learn more. ## Anatomy# * * * Toggle buttons consist of a clickable area usually containing a textual label or an icon that users can click to toggle a selection state. In addition, keyboard users may toggle the state using the Space or Enter keys. If a visual label is not provided (e.g. an icon only button), then an `aria-label` or `aria-labelledby` prop must be passed to identify the button to assistive technology. ## Starter kits# * * * To help kick-start your project, we offer starter kits that include example implementations of all React Aria components with various styling solutions. All components are fully styled, including support for dark mode, high contrast mode, and all UI states. Each starter comes with a pre-configured Storybook that you can experiment with, or use as a starting point for your own component library. Vanilla CSS Download ZIP Preview Tailwind CSS Download ZIP Preview ## Selection# * * * A default selection state for a toggle button can be set using the `defaultSelected` prop, or controlled with the `isSelected` prop. The `onChange` event is fired when the user presses the button, toggling the boolean. See React's documentation on uncontrolled components for more info. function Example() { let [isSelected, setSelected] = React.useState(false); return ( <ToggleButton isSelected={isSelected} onChange={setSelected} aria-label="Star"> ★ </ToggleButton> ); } function Example() { let [isSelected, setSelected] = React.useState(false); return ( <ToggleButton isSelected={isSelected} onChange={setSelected} aria-label="Star"> ★ </ToggleButton> ); } function Example() { let [ isSelected, setSelected ] = React.useState( false ); return ( <ToggleButton isSelected={isSelected} onChange={setSelected} aria-label="Star" > ★ </ToggleButton> ); } ★ ## Disabled# * * * A `ToggleButton` can be disabled using the `isDisabled` prop. <ToggleButton isDisabled>Pin</ToggleButton> <ToggleButton isDisabled>Pin</ToggleButton> <ToggleButton isDisabled > Pin </ToggleButton> Pin Show CSS .react-aria-ToggleButton { &[data-disabled] { border-color: var(--border-color-disabled); background: var(--button-background); color: var(--text-color-disabled); } } .react-aria-ToggleButton { &[data-disabled] { border-color: var(--border-color-disabled); background: var(--button-background); color: var(--text-color-disabled); } } .react-aria-ToggleButton { &[data-disabled] { border-color: var(--border-color-disabled); background: var(--button-background); color: var(--text-color-disabled); } } ## Props# * * * | Name | Type | Default | Description | | --- | --- | --- | --- | | `id` | `Key` | — | When used in a ToggleButtonGroup, an identifier for the item in `selectedKeys`. When used standalone, a DOM id. | | `isSelected` | `boolean` | — | Whether the element should be selected (controlled). | | `defaultSelected` | `boolean` | — | Whether the element should be selected (uncontrolled). | | `isDisabled` | `boolean` | — | Whether the button is disabled. | | `autoFocus` | `boolean` | — | Whether the element should receive focus on render. | | `type` | `'button' | 'submit' | 'reset'` | `'button'` | The behavior of the button when used in an HTML form. | | `children` | `ReactNode | ( (values: ToggleButtonRenderProps & & { defaultChildren: ReactNode | | undefined } )) => ReactNode` | — | The children of the component. A function may be provided to alter the children based on component state. | | `className` | `string | ( (values: ToggleButtonRenderProps & & { defaultClassName: string | | undefined } )) => string` | — | The CSS className for the element. A function may be provided to compute the class based on component state. | | `style` | `CSSProperties | ( (values: ToggleButtonRenderProps & & { defaultStyle: CSSProperties } )) => CSSProperties | undefined` | — | The inline style for the element. A function may be provided to compute the style based on component state. | Events | Name | Type | Description | | --- | --- | --- | | `onChange` | `( (isSelected: boolean )) => void` | Handler that is called when the element's selection state changes. | | `onPress` | `( (e: PressEvent )) => void` | Handler that is called when the press is released over the target. | | `onPressStart` | `( (e: PressEvent )) => void` | Handler that is called when a press interaction starts. | | `onPressEnd` | `( (e: PressEvent )) => void` | Handler that is called when a press interaction ends, either over the target or when the pointer leaves the target. | | `onPressChange` | `( (isPressed: boolean )) => void` | Handler that is called when the press state changes. | | `onPressUp` | `( (e: PressEvent )) => void` | Handler that is called when a press is released over the target, regardless of whether it started on the target or not. | | `onClick` | `( (e: MouseEvent<FocusableElement> )) => void` | **Not recommended – use `onPress` instead.** `onClick` is an alias for `onPress` provided for compatibility with other libraries. `onPress` provides additional event details for non-mouse interactions. | | `onFocus` | `( (e: FocusEvent<Target> )) => void` | Handler that is called when the element receives focus. | | `onBlur` | `( (e: FocusEvent<Target> )) => void` | Handler that is called when the element loses focus. | | `onFocusChange` | `( (isFocused: boolean )) => void` | Handler that is called when the element's focus status changes. | | `onKeyDown` | `( (e: KeyboardEvent )) => void` | Handler that is called when a key is pressed. | | `onKeyUp` | `( (e: KeyboardEvent )) => void` | Handler that is called when a key is released. | | `onHoverStart` | `( (e: HoverEvent )) => void` | Handler that is called when a hover interaction starts. | | `onHoverEnd` | `( (e: HoverEvent )) => void` | Handler that is called when a hover interaction ends. | | `onHoverChange` | `( (isHovering: boolean )) => void` | Handler that is called when the hover state changes. | Layout | Name | Type | Description | | --- | --- | --- | | `slot` | `string | null` | A slot name for the component. Slots allow the component to receive props from a parent component. An explicit `null` value indicates that the local props completely override all props received from a parent. | Accessibility | Name | Type | Description | | --- | --- | --- | | `excludeFromTabOrder` | `boolean` | Whether to exclude the element from the sequential tab order. If true, the element will not be focusable via the keyboard by tabbing. This should be avoided except in rare scenarios where an alternative means of accessing the element or its functionality via the keyboard is available. | | `preventFocusOnPress` | `boolean` | Whether to prevent focus from moving to the button when pressing it. Caution, this can make the button inaccessible and should only be used when alternative keyboard interaction is provided, such as ComboBox's MenuTrigger or a NumberField's increment/decrement control. | | `aria-expanded` | `boolean | 'true' | 'false'` | Indicates whether the element, or another grouping element it controls, is currently expanded or collapsed. | | `aria-haspopup` | `boolean | 'menu' | 'listbox' | 'tree' | 'grid' | 'dialog' | 'true' | 'false'` | Indicates the availability and type of interactive popup element, such as menu or dialog, that can be triggered by an element. | | `aria-controls` | `string` | Identifies the element (or elements) whose contents or presence are controlled by the current element. | | `aria-pressed` | `boolean | 'true' | 'false' | 'mixed'` | Indicates the current "pressed" state of toggle buttons. | | `aria-label` | `string` | Defines a string value that labels the current element. | | `aria-labelledby` | `string` | Identifies the element (or elements) that labels the current element. | | `aria-describedby` | `string` | Identifies the element (or elements) that describes the object. | | `aria-details` | `string` | Identifies the element (or elements) that provide a detailed, extended description for the object. | ## Styling# * * * React Aria components can be styled in many ways, including using CSS classes, inline styles, utility classes (e.g. Tailwind), CSS-in-JS (e.g. Styled Components), etc. By default, all components include a builtin `className` attribute which can be targeted using CSS selectors. These follow the `react-aria-ComponentName` naming convention. .react-aria-ToggleButton { /* ... */ } .react-aria-ToggleButton { /* ... */ } .react-aria-ToggleButton { /* ... */ } A custom `className` can also be specified on any component. This overrides the default `className` provided by React Aria with your own. <ToggleButton className="my-button"> {/* ... */} </ToggleButton> <ToggleButton className="my-button"> {/* ... */} </ToggleButton> <ToggleButton className="my-button"> {/* ... */} </ToggleButton> In addition, some components support multiple UI states (e.g. focused, placeholder, readonly, etc.). React Aria components expose states using data attributes, which you can target in CSS selectors. For example: .react-aria-ToggleButton[data-selected] { /* ... */ } .react-aria-ToggleButton[data-selected] { /* ... */ } .react-aria-ToggleButton[data-selected] { /* ... */ } The `className` and `style` props also accept functions which receive states for styling. This lets you dynamically determine the classes or styles to apply, which is useful when using utility CSS libraries like Tailwind. <ToggleButton className={({ isSelected }) => isSelected ? 'bg-blue-600' : 'bg-gray-600'} /> <ToggleButton className={({ isSelected }) => isSelected ? 'bg-blue-600' : 'bg-gray-600'} /> <ToggleButton className={( { isSelected } ) => isSelected ? 'bg-blue-600' : 'bg-gray-600'} /> Render props may also be used as children to alter what elements are rendered based on the current state. For example, you could swap an icon depending on the selection state. <ToggleButton> {({isSelected}) => ( <> {isSelected ? <PinnedIcon /> : <UnpinnedIcon />} Pin </> )} </ToggleButton> <ToggleButton> {({isSelected}) => ( <> {isSelected ? <PinnedIcon /> : <UnpinnedIcon />} Pin </> )} </ToggleButton> <ToggleButton> {( { isSelected } ) => ( <> {isSelected ? ( <PinnedIcon /> ) : ( <UnpinnedIcon /> )} Pin </> )} </ToggleButton> The states, selectors, and render props for `ToggleButton` are documented below. | Name | CSS Selector | Description | | --- | --- | --- | | `isSelected` | `[data-selected]` | Whether the button is currently selected. | | `state` | `—` | State of the toggle button. | | `isHovered` | `[data-hovered]` | Whether the button is currently hovered with a mouse. | | `isPressed` | `[data-pressed]` | Whether the button is currently in a pressed state. | | `isFocused` | `[data-focused]` | Whether the button is focused, either via a mouse or keyboard. | | `isFocusVisible` | `[data-focus-visible]` | Whether the button is keyboard focused. | | `isDisabled` | `[data-disabled]` | Whether the button is disabled. | ## Advanced customization# * * * ### Contexts# All React Aria Components export a corresponding context that can be used to send props to them from a parent element. This enables you to build your own compositional APIs similar to those found in React Aria Components itself. You can send any prop or ref via context that you could pass to the corresponding component. The local props and ref on the component are merged with the ones passed via context, with the local props taking precedence (following the rules documented in mergeProps). | Component | Context | Props | Ref | | --- | --- | --- | --- | | `ToggleButton` | `ToggleButtonContext` | ` ToggleButtonProps ` | `HTMLButtonElement` | This example shows a `ButtonGroup` component that renders a group of buttons. The entire group can be marked as disabled via the `isDisabled` prop, which is passed to all child buttons via the `ButtonContext` provider. import {ToggleButtonContext} from 'react-aria-components'; interface ButtonGroupProps { children?: React.ReactNode, isDisabled?: boolean } function ButtonGroup({children, isDisabled}: ButtonGroupProps) { return ( <div style={{display: 'flex', gap: 8}}> <ToggleButtonContext.Provider value={{isDisabled}}> {children} </ToggleButtonContext.Provider> </div> ); } <ButtonGroup isDisabled> <ToggleButton isSelected aria-label="Favorite">★</ToggleButton> <ToggleButton aria-label="Flag">⚑</ToggleButton> </ButtonGroup> import {ToggleButtonContext} from 'react-aria-components'; interface ButtonGroupProps { children?: React.ReactNode; isDisabled?: boolean; } function ButtonGroup( { children, isDisabled }: ButtonGroupProps ) { return ( <div style={{ display: 'flex', gap: 8 }}> <ToggleButtonContext.Provider value={{ isDisabled }}> {children} </ToggleButtonContext.Provider> </div> ); } <ButtonGroup isDisabled> <ToggleButton isSelected aria-label="Favorite"> ★ </ToggleButton> <ToggleButton aria-label="Flag">⚑</ToggleButton> </ButtonGroup> import {ToggleButtonContext} from 'react-aria-components'; interface ButtonGroupProps { children?: React.ReactNode; isDisabled?: boolean; } function ButtonGroup( { children, isDisabled }: ButtonGroupProps ) { return ( <div style={{ display: 'flex', gap: 8 }} > <ToggleButtonContext.Provider value={{ isDisabled }} > {children} </ToggleButtonContext.Provider> </div> ); } <ButtonGroup isDisabled > <ToggleButton isSelected aria-label="Favorite" > ★ </ToggleButton> <ToggleButton aria-label="Flag"> ⚑ </ToggleButton> </ButtonGroup> ★⚑ ### Hooks# If you need to customize things further, such as intercepting events or customizing DOM elements, you can drop down to the lower level Hook-based API. Consume from `ToggleButtonContext` in your component with `useContextProps` to make it compatible with other React Aria Components. See useToggleButton for more details. This example uses Framer Motion to create an `AnimatedToggleButton` component that animates based on the `isSelected` state. It can be used standalone or as a part of any React Aria component. import type {ToggleButtonProps} from 'react-aria-components'; import {ToggleButtonContext, useContextProps} from 'react-aria-components'; import {useToggleState} from 'react-stately'; import {useToggleButton} from 'react-aria'; import {motion} from 'framer-motion'; const AnimatedToggleButton = React.forwardRef( (props: ToggleButtonProps, ref: React.ForwardedRef<HTMLButtonElement>) => { // Merge the local props and ref with the ones provided via context. [props, ref] = useContextProps(props, ref, ButtonContext); let state = useToggleState(props); let { buttonProps } = useToggleButton(props, state, ref); return ( <motion.button {...buttonProps} ref={ref} animate={{ scale: state.isSelected ? 1.2 : 1 }} > {props.children} </motion.button> ); } ); import type {ToggleButtonProps} from 'react-aria-components'; import { ToggleButtonContext, useContextProps } from 'react-aria-components'; import {useToggleState} from 'react-stately'; import {useToggleButton} from 'react-aria'; import {motion} from 'framer-motion'; const AnimatedToggleButton = React.forwardRef( ( props: ToggleButtonProps, ref: React.ForwardedRef<HTMLButtonElement> ) => { // Merge the local props and ref with the ones provided via context. [props, ref] = useContextProps( props, ref, ButtonContext ); let state = useToggleState(props); let { buttonProps } = useToggleButton( props, state, ref ); return ( <motion.button {...buttonProps} ref={ref} animate={{ scale: state.isSelected ? 1.2 : 1 }} > {props.children} </motion.button> ); } ); import type {ToggleButtonProps} from 'react-aria-components'; import { ToggleButtonContext, useContextProps } from 'react-aria-components'; import {useToggleState} from 'react-stately'; import {useToggleButton} from 'react-aria'; import {motion} from 'framer-motion'; const AnimatedToggleButton = React.forwardRef( ( props: ToggleButtonProps, ref: React.ForwardedRef< HTMLButtonElement > ) => { // Merge the local props and ref with the ones provided via context. [props, ref] = useContextProps( props, ref, ButtonContext ); let state = useToggleState( props ); let { buttonProps } = useToggleButton( props, state, ref ); return ( <motion.button {...buttonProps} ref={ref} animate={{ scale: state .isSelected ? 1.2 : 1 }} > {props .children} </motion.button> ); } ); --- ## Page: https://react-spectrum.adobe.com/react-aria/ToggleButtonGroup.html # ToggleButtonGroup A toggle button group allows a user to toggle multiple options, with single or multiple selection. <table><tbody><tr><th>install</th><td><code>yarn add react-aria-components</code></td></tr><tr><th>version</th><td>1.10.1</td></tr><tr><th>usage</th><td><code><span>import</span> {ToggleButtonGroup} <span>from</span> <span>'react-aria-components'</span></code></td></tr></tbody></table> View ARIA pattern W3C View repository GitHub View package NPM ## Example# * * * import {ToggleButtonGroup, ToggleButton} from 'react-aria-components'; <ToggleButtonGroup> <ToggleButton id="left">Left</ToggleButton> <ToggleButton id="center">Center</ToggleButton> <ToggleButton id="right">Right</ToggleButton> </ToggleButtonGroup> import { ToggleButton, ToggleButtonGroup } from 'react-aria-components'; <ToggleButtonGroup> <ToggleButton id="left">Left</ToggleButton> <ToggleButton id="center">Center</ToggleButton> <ToggleButton id="right">Right</ToggleButton> </ToggleButtonGroup> import { ToggleButton, ToggleButtonGroup } from 'react-aria-components'; <ToggleButtonGroup> <ToggleButton id="left"> Left </ToggleButton> <ToggleButton id="center"> Center </ToggleButton> <ToggleButton id="right"> Right </ToggleButton> </ToggleButtonGroup> LeftCenterRight Show CSS .react-aria-ToggleButtonGroup { display: flex; > button { border-radius: 0; z-index: 1; &[data-disabled] { z-index: 0; } &[data-selected], &[data-focus-visible] { z-index: 2; } } } .react-aria-ToggleButtonGroup[data-orientation=horizontal] { flex-direction: row; > button { margin-inline-start: -1px; &:first-child { border-radius: 4px 0 0 4px; margin-inline-start: 0; } &:last-child { border-radius: 0 4px 4px 0; } } } .react-aria-ToggleButtonGroup { display: flex; > button { border-radius: 0; z-index: 1; &[data-disabled] { z-index: 0; } &[data-selected], &[data-focus-visible] { z-index: 2; } } } .react-aria-ToggleButtonGroup[data-orientation=horizontal] { flex-direction: row; > button { margin-inline-start: -1px; &:first-child { border-radius: 4px 0 0 4px; margin-inline-start: 0; } &:last-child { border-radius: 0 4px 4px 0; } } } .react-aria-ToggleButtonGroup { display: flex; > button { border-radius: 0; z-index: 1; &[data-disabled] { z-index: 0; } &[data-selected], &[data-focus-visible] { z-index: 2; } } } .react-aria-ToggleButtonGroup[data-orientation=horizontal] { flex-direction: row; > button { margin-inline-start: -1px; &:first-child { border-radius: 4px 0 0 4px; margin-inline-start: 0; } &:last-child { border-radius: 0 4px 4px 0; } } } ## Features# * * * There is no built in element for toggle button groups in HTML. `ToggleButtonGroup` helps achieve accessible toggle button groups that can be styled as needed. * **Accessible** – Represented as an ARIA radiogroup when using single selection, or a toolbar when using multiple selection. * **Keyboard navigation** – Users can navigate between buttons with the arrow keys. Selection can be toggled using the Enter or Space keys. * **Styleable** – Hover, press, keyboard focus, and selection states are provided for easy styling. ## Anatomy# * * * A toggle button group consists of a set of toggle buttons, and coordinates the selection state between them. Users can navigate between buttons with the arrow keys in either horizontal or vertical orientations. import {ToggleButtonGroup, ToggleButton} from 'react-aria-components'; <ToggleButtonGroup> <ToggleButton /> </ToggleButtonGroup> import { ToggleButton, ToggleButtonGroup } from 'react-aria-components'; <ToggleButtonGroup> <ToggleButton /> </ToggleButtonGroup> import { ToggleButton, ToggleButtonGroup } from 'react-aria-components'; <ToggleButtonGroup> <ToggleButton /> </ToggleButtonGroup> ### Composed Components# ToggleButton A toggle button allows a user to toggle between two states. ## Selection# * * * ToggleButtonGroup supports both single and multiple selection modes. Use `defaultSelectedKeys` to provide a default set of selected items (uncontrolled) and `selectedKeys` to set the selected items (controlled). The value of the selected keys must match the `id` prop of the items. ### Single selection# By default, the `selectionMode` of a `ToggleButtonGroup` is `"single"`. <ToggleButtonGroup defaultSelectedKeys={['list']}> <ToggleButton id="grid">Grid view</ToggleButton> <ToggleButton id="list">List view</ToggleButton> <ToggleButton id="gallery">Gallery view</ToggleButton> </ToggleButtonGroup> <ToggleButtonGroup defaultSelectedKeys={['list']}> <ToggleButton id="grid">Grid view</ToggleButton> <ToggleButton id="list">List view</ToggleButton> <ToggleButton id="gallery">Gallery view</ToggleButton> </ToggleButtonGroup> <ToggleButtonGroup defaultSelectedKeys={[ 'list' ]} > <ToggleButton id="grid"> Grid view </ToggleButton> <ToggleButton id="list"> List view </ToggleButton> <ToggleButton id="gallery"> Gallery view </ToggleButton> </ToggleButtonGroup> Grid viewList viewGallery view ### Multiple selection# Set `selectionMode` prop to `multiple` to allow more than one selection. <ToggleButtonGroup selectionMode="multiple"> <ToggleButton id="bold">Bold</ToggleButton> <ToggleButton id="italic">Italic</ToggleButton> <ToggleButton id="underline">Underline</ToggleButton> </ToggleButtonGroup> <ToggleButtonGroup selectionMode="multiple"> <ToggleButton id="bold">Bold</ToggleButton> <ToggleButton id="italic">Italic</ToggleButton> <ToggleButton id="underline">Underline</ToggleButton> </ToggleButtonGroup> <ToggleButtonGroup selectionMode="multiple"> <ToggleButton id="bold"> Bold </ToggleButton> <ToggleButton id="italic"> Italic </ToggleButton> <ToggleButton id="underline"> Underline </ToggleButton> </ToggleButtonGroup> BoldItalicUnderline ### Controlled selection# The `selectedKeys` prop can be used to make the selected state controlled. import type {Key} from 'react-aria-components'; function Example() { let [selected, setSelected] = React.useState(new Set<Key>(['bold'])); return ( <> <ToggleButtonGroup selectionMode="multiple" selectedKeys={selected} onSelectionChange={setSelected} > <ToggleButton id="bold">Bold</ToggleButton> <ToggleButton id="italic">Italic</ToggleButton> <ToggleButton id="underline">Underline</ToggleButton> </ToggleButtonGroup> <p>Current selections (controlled): {[...selected].join(', ')}</p> </> ); } import type {Key} from 'react-aria-components'; function Example() { let [selected, setSelected] = React.useState( new Set<Key>(['bold']) ); return ( <> <ToggleButtonGroup selectionMode="multiple" selectedKeys={selected} onSelectionChange={setSelected} > <ToggleButton id="bold">Bold</ToggleButton> <ToggleButton id="italic">Italic</ToggleButton> <ToggleButton id="underline"> Underline </ToggleButton> </ToggleButtonGroup> <p> Current selections (controlled):{' '} {[...selected].join(', ')} </p> </> ); } import type {Key} from 'react-aria-components'; function Example() { let [ selected, setSelected ] = React.useState( new Set<Key>([ 'bold' ]) ); return ( <> <ToggleButtonGroup selectionMode="multiple" selectedKeys={selected} onSelectionChange={setSelected} > <ToggleButton id="bold"> Bold </ToggleButton> <ToggleButton id="italic"> Italic </ToggleButton> <ToggleButton id="underline"> Underline </ToggleButton> </ToggleButtonGroup> <p> Current selections (controlled): {' '} {[...selected] .join(', ')} </p> </> ); } BoldItalicUnderline Current selections (controlled): bold ## Disabled# * * * All buttons within a `ToggleButtonGroup` can be disabled using the `isDisabled` prop. <ToggleButtonGroup isDisabled> <ToggleButton id="grid">Grid view</ToggleButton> <ToggleButton id="list">List view</ToggleButton> <ToggleButton id="gallery">Gallery view</ToggleButton> </ToggleButtonGroup> <ToggleButtonGroup isDisabled> <ToggleButton id="grid">Grid view</ToggleButton> <ToggleButton id="list">List view</ToggleButton> <ToggleButton id="gallery">Gallery view</ToggleButton> </ToggleButtonGroup> <ToggleButtonGroup isDisabled > <ToggleButton id="grid"> Grid view </ToggleButton> <ToggleButton id="list"> List view </ToggleButton> <ToggleButton id="gallery"> Gallery view </ToggleButton> </ToggleButtonGroup> Grid viewList viewGallery view Individual items can be disabled using the `isDisabled` prop on each `ToggleButton`. <ToggleButtonGroup> <ToggleButton id="grid">Grid view</ToggleButton> <ToggleButton id="list" isDisabled>List view</ToggleButton> <ToggleButton id="gallery">Gallery view</ToggleButton> </ToggleButtonGroup> <ToggleButtonGroup> <ToggleButton id="grid">Grid view</ToggleButton> <ToggleButton id="list" isDisabled> List view </ToggleButton> <ToggleButton id="gallery">Gallery view</ToggleButton> </ToggleButtonGroup> <ToggleButtonGroup> <ToggleButton id="grid"> Grid view </ToggleButton> <ToggleButton id="list" isDisabled > List view </ToggleButton> <ToggleButton id="gallery"> Gallery view </ToggleButton> </ToggleButtonGroup> Grid viewList viewGallery view ## Orientation# * * * By default, toggle button groups are horizontally oriented. The orientation prop can be set to "vertical" to change the arrow key navigation behavior. <ToggleButtonGroup orientation="vertical"> <ToggleButton id="grid">Grid</ToggleButton> <ToggleButton id="list">List</ToggleButton> <ToggleButton id="gallery">Gallery</ToggleButton> </ToggleButtonGroup> <ToggleButtonGroup orientation="vertical"> <ToggleButton id="grid">Grid</ToggleButton> <ToggleButton id="list">List</ToggleButton> <ToggleButton id="gallery">Gallery</ToggleButton> </ToggleButtonGroup> <ToggleButtonGroup orientation="vertical"> <ToggleButton id="grid"> Grid </ToggleButton> <ToggleButton id="list"> List </ToggleButton> <ToggleButton id="gallery"> Gallery </ToggleButton> </ToggleButtonGroup> GridListGallery Show CSS .react-aria-ToggleButtonGroup[data-orientation=vertical] { flex-direction: column; width: fit-content; > button { margin-block-start: -1px; &:first-child { border-radius: 4px 4px 0 0; margin-block-start: 0; } &:last-child { border-radius: 0 0 4px 4px; } } } .react-aria-ToggleButtonGroup[data-orientation=vertical] { flex-direction: column; width: fit-content; > button { margin-block-start: -1px; &:first-child { border-radius: 4px 4px 0 0; margin-block-start: 0; } &:last-child { border-radius: 0 0 4px 4px; } } } .react-aria-ToggleButtonGroup[data-orientation=vertical] { flex-direction: column; width: fit-content; > button { margin-block-start: -1px; &:first-child { border-radius: 4px 4px 0 0; margin-block-start: 0; } &:last-child { border-radius: 0 0 4px 4px; } } } ## Accessiblity# * * * A `ToggleButtonGroup` can be labeled using the `aria-label` or `aria-labelledby` props. <ToggleButtonGroup aria-label="Text style"> <ToggleButton id="bold">Bold</ToggleButton> <ToggleButton id="italic">Italic</ToggleButton> <ToggleButton id="underline">Underline</ToggleButton> </ToggleButtonGroup> <ToggleButtonGroup aria-label="Text style"> <ToggleButton id="bold">Bold</ToggleButton> <ToggleButton id="italic">Italic</ToggleButton> <ToggleButton id="underline">Underline</ToggleButton> </ToggleButtonGroup> <ToggleButtonGroup aria-label="Text style"> <ToggleButton id="bold"> Bold </ToggleButton> <ToggleButton id="italic"> Italic </ToggleButton> <ToggleButton id="underline"> Underline </ToggleButton> </ToggleButtonGroup> BoldItalicUnderline ## Props# * * * ### ToggleButtonGroup# | Name | Type | Default | Description | | --- | --- | --- | --- | | `orientation` | ` Orientation ` | `'horizontal'` | The orientation of the the toggle button group. | | `selectionMode` | `'single' | 'multiple'` | — | Whether single or multiple selection is enabled. | | `disallowEmptySelection` | `boolean` | — | Whether the collection allows empty selection. | | `selectedKeys` | `Iterable<Key>` | — | The currently selected keys in the collection (controlled). | | `defaultSelectedKeys` | `Iterable<Key>` | — | The initial selected keys in the collection (uncontrolled). | | `isDisabled` | `boolean` | — | Whether all items are disabled. | | `children` | `ReactNode | ( (values: ToggleButtonGroupRenderProps & & { defaultChildren: ReactNode | | undefined } )) => ReactNode` | — | The children of the component. A function may be provided to alter the children based on component state. | | `className` | `string | ( (values: ToggleButtonGroupRenderProps & & { defaultClassName: string | | undefined } )) => string` | — | The CSS className for the element. A function may be provided to compute the class based on component state. | | `style` | `CSSProperties | ( (values: ToggleButtonGroupRenderProps & & { defaultStyle: CSSProperties } )) => CSSProperties | undefined` | — | The inline style for the element. A function may be provided to compute the style based on component state. | Events | Name | Type | Description | | --- | --- | --- | | `onSelectionChange` | `( (keys: Set<Key> )) => void` | Handler that is called when the selection changes. | Layout | Name | Type | Description | | --- | --- | --- | | `slot` | `string | null` | A slot name for the component. Slots allow the component to receive props from a parent component. An explicit `null` value indicates that the local props completely override all props received from a parent. | Accessibility | Name | Type | Description | | --- | --- | --- | | `aria-label` | `string` | Defines a string value that labels the current element. | | `aria-labelledby` | `string` | Identifies the element (or elements) that labels the current element. | | `aria-describedby` | `string` | Identifies the element (or elements) that describes the object. | | `aria-details` | `string` | Identifies the element (or elements) that provide a detailed, extended description for the object. | ### ToggleButton# | Name | Type | Default | Description | | --- | --- | --- | --- | | `id` | `Key` | — | When used in a ToggleButtonGroup, an identifier for the item in `selectedKeys`. When used standalone, a DOM id. | | `isSelected` | `boolean` | — | Whether the element should be selected (controlled). | | `defaultSelected` | `boolean` | — | Whether the element should be selected (uncontrolled). | | `isDisabled` | `boolean` | — | Whether the button is disabled. | | `autoFocus` | `boolean` | — | Whether the element should receive focus on render. | | `type` | `'button' | 'submit' | 'reset'` | `'button'` | The behavior of the button when used in an HTML form. | | `children` | `ReactNode | ( (values: ToggleButtonRenderProps & & { defaultChildren: ReactNode | | undefined } )) => ReactNode` | — | The children of the component. A function may be provided to alter the children based on component state. | | `className` | `string | ( (values: ToggleButtonRenderProps & & { defaultClassName: string | | undefined } )) => string` | — | The CSS className for the element. A function may be provided to compute the class based on component state. | | `style` | `CSSProperties | ( (values: ToggleButtonRenderProps & & { defaultStyle: CSSProperties } )) => CSSProperties | undefined` | — | The inline style for the element. A function may be provided to compute the style based on component state. | Events | Name | Type | Description | | --- | --- | --- | | `onChange` | `( (isSelected: boolean )) => void` | Handler that is called when the element's selection state changes. | | `onPress` | `( (e: PressEvent )) => void` | Handler that is called when the press is released over the target. | | `onPressStart` | `( (e: PressEvent )) => void` | Handler that is called when a press interaction starts. | | `onPressEnd` | `( (e: PressEvent )) => void` | Handler that is called when a press interaction ends, either over the target or when the pointer leaves the target. | | `onPressChange` | `( (isPressed: boolean )) => void` | Handler that is called when the press state changes. | | `onPressUp` | `( (e: PressEvent )) => void` | Handler that is called when a press is released over the target, regardless of whether it started on the target or not. | | `onClick` | `( (e: MouseEvent<FocusableElement> )) => void` | **Not recommended – use `onPress` instead.** `onClick` is an alias for `onPress` provided for compatibility with other libraries. `onPress` provides additional event details for non-mouse interactions. | | `onFocus` | `( (e: FocusEvent<Target> )) => void` | Handler that is called when the element receives focus. | | `onBlur` | `( (e: FocusEvent<Target> )) => void` | Handler that is called when the element loses focus. | | `onFocusChange` | `( (isFocused: boolean )) => void` | Handler that is called when the element's focus status changes. | | `onKeyDown` | `( (e: KeyboardEvent )) => void` | Handler that is called when a key is pressed. | | `onKeyUp` | `( (e: KeyboardEvent )) => void` | Handler that is called when a key is released. | | `onHoverStart` | `( (e: HoverEvent )) => void` | Handler that is called when a hover interaction starts. | | `onHoverEnd` | `( (e: HoverEvent )) => void` | Handler that is called when a hover interaction ends. | | `onHoverChange` | `( (isHovering: boolean )) => void` | Handler that is called when the hover state changes. | Layout | Name | Type | Description | | --- | --- | --- | | `slot` | `string | null` | A slot name for the component. Slots allow the component to receive props from a parent component. An explicit `null` value indicates that the local props completely override all props received from a parent. | Accessibility | Name | Type | Description | | --- | --- | --- | | `excludeFromTabOrder` | `boolean` | Whether to exclude the element from the sequential tab order. If true, the element will not be focusable via the keyboard by tabbing. This should be avoided except in rare scenarios where an alternative means of accessing the element or its functionality via the keyboard is available. | | `preventFocusOnPress` | `boolean` | Whether to prevent focus from moving to the button when pressing it. Caution, this can make the button inaccessible and should only be used when alternative keyboard interaction is provided, such as ComboBox's MenuTrigger or a NumberField's increment/decrement control. | | `aria-expanded` | `boolean | 'true' | 'false'` | Indicates whether the element, or another grouping element it controls, is currently expanded or collapsed. | | `aria-haspopup` | `boolean | 'menu' | 'listbox' | 'tree' | 'grid' | 'dialog' | 'true' | 'false'` | Indicates the availability and type of interactive popup element, such as menu or dialog, that can be triggered by an element. | | `aria-controls` | `string` | Identifies the element (or elements) whose contents or presence are controlled by the current element. | | `aria-pressed` | `boolean | 'true' | 'false' | 'mixed'` | Indicates the current "pressed" state of toggle buttons. | | `aria-label` | `string` | Defines a string value that labels the current element. | | `aria-labelledby` | `string` | Identifies the element (or elements) that labels the current element. | | `aria-describedby` | `string` | Identifies the element (or elements) that describes the object. | | `aria-details` | `string` | Identifies the element (or elements) that provide a detailed, extended description for the object. | ## Styling# * * * React Aria components can be styled in many ways, including using CSS classes, inline styles, utility classes (e.g. Tailwind), CSS-in-JS (e.g. Styled Components), etc. By default, all components include a builtin `className` attribute which can be targeted using CSS selectors. These follow the `react-aria-ComponentName` naming convention. .react-aria-ToggleButtonGroup { /* ... */ } .react-aria-ToggleButtonGroup { /* ... */ } .react-aria-ToggleButtonGroup { /* ... */ } A custom `className` can also be specified on any component. This overrides the default `className` provided by React Aria with your own. <ToggleButtonGroup className="my-toggle-group"> {/* ... */} </ToggleButtonGroup> <ToggleButtonGroup className="my-toggle-group"> {/* ... */} </ToggleButtonGroup> <ToggleButtonGroup className="my-toggle-group"> {/* ... */} </ToggleButtonGroup> In addition, some components support multiple UI states (e.g. focused, placeholder, readonly, etc.). React Aria components expose states using data attributes, which you can target in CSS selectors. For example: .react-aria-ToggleButton[data-selected] { /* ... */ } .react-aria-ToggleButton[data-selected] { /* ... */ } .react-aria-ToggleButton[data-selected] { /* ... */ } The `className` and `style` props also accept functions which receive states for styling. This lets you dynamically determine the classes or styles to apply, which is useful when using utility CSS libraries like Tailwind. <ToggleButtonGroup className={({ isDisabled }) => isDisabled ? 'bg-gray-100' : 'bg-gray-600'} /> <ToggleButtonGroup className={({ isDisabled }) => isDisabled ? 'bg-gray-100' : 'bg-gray-600'} /> <ToggleButtonGroup className={( { isDisabled } ) => isDisabled ? 'bg-gray-100' : 'bg-gray-600'} /> Render props may also be used as children to alter what elements are rendered based on the current state. For example, you could swap an icon depending on the selection state. <ToggleButton> {({isSelected}) => ( <> {isSelected ? <PinnedIcon /> : <UnpinnedIcon />} Pin </> )} </ToggleButton> <ToggleButton> {({isSelected}) => ( <> {isSelected ? <PinnedIcon /> : <UnpinnedIcon />} Pin </> )} </ToggleButton> <ToggleButton> {( { isSelected } ) => ( <> {isSelected ? ( <PinnedIcon /> ) : ( <UnpinnedIcon /> )} Pin </> )} </ToggleButton> The states, selectors, and render props for each component used in a `ToggleButtonGroup` are documented below. ### ToggleButtonGroup# A `ToggleButtonGroup` can be targeted with the `.react-aria-ToggleButtonGroup` CSS selector, or by overriding with a custom `className`. It supports the following states and render props: | Name | CSS Selector | Description | | --- | --- | --- | | `isDisabled` | `[data-disabled]` | Whether the toggle button group is disabled. | | `state` | `—` | State of the toggle button group. | ### ToggleButton# A `ToggleButton` can be targeted with the `.react-aria-ToggleButton` CSS selector, or by overriding with a custom `className`. | Name | CSS Selector | Description | | --- | --- | --- | | `isSelected` | `[data-selected]` | Whether the button is currently selected. | | `state` | `—` | State of the toggle button. | | `isHovered` | `[data-hovered]` | Whether the button is currently hovered with a mouse. | | `isPressed` | `[data-pressed]` | Whether the button is currently in a pressed state. | | `isFocused` | `[data-focused]` | Whether the button is focused, either via a mouse or keyboard. | | `isFocusVisible` | `[data-focus-visible]` | Whether the button is keyboard focused. | | `isDisabled` | `[data-disabled]` | Whether the button is disabled. | ## Advanced customization# * * * ### Contexts# All React Aria Components export a corresponding context that can be used to send props to them from a parent element. This enables you to build your own compositional APIs similar to those found in React Aria Components itself. You can send any prop or ref via context that you could pass to the corresponding component. The local props and ref on the component are merged with the ones passed via context, with the local props taking precedence (following the rules documented in mergeProps). | Component | Context | Props | Ref | | --- | --- | --- | --- | | `ToggleButtonGroup` | `ToggleButtonGroupContext` | ` ToggleButtonGroupProps ` | `HTMLDivElement` | | `ToggleButton` | `ToggleButtonContext` | ` ToggleButtonProps ` | `HTMLButtonElement` | ### State# ToggleButtonGroup provides an `ToggleGroupState` object to its children via `ToggleGroupStateContext`. This can be used to access and manipulate the toggle button group's state. This example shows a `ClearButton` component that can be placed within a `ToggleButtonGroup` to allow the user to clear the selected item. import {Button, ToggleGroupStateContext} from 'react-aria-components'; function ClearButton() { let state = React.useContext(ToggleGroupStateContext); return ( <Button onPress={() => state?.setSelectedKeys(new Set())}> Clear </Button> ); } <ToggleButtonGroup selectionMode="multiple" defaultSelectedKeys={['bold', 'italic']} > <ToggleButton id="bold">Bold</ToggleButton> <ToggleButton id="italic">Italic</ToggleButton> <ToggleButton id="underline">Underline</ToggleButton> <ClearButton /></ToggleButtonGroup> import { Button, ToggleGroupStateContext } from 'react-aria-components'; function ClearButton() { let state = React.useContext(ToggleGroupStateContext); return ( <Button onPress={() => state?.setSelectedKeys(new Set())} > Clear </Button> ); } <ToggleButtonGroup selectionMode="multiple" defaultSelectedKeys={['bold', 'italic']} > <ToggleButton id="bold">Bold</ToggleButton> <ToggleButton id="italic">Italic</ToggleButton> <ToggleButton id="underline">Underline</ToggleButton> <ClearButton /></ToggleButtonGroup> import { Button, ToggleGroupStateContext } from 'react-aria-components'; function ClearButton() { let state = React .useContext( ToggleGroupStateContext ); return ( <Button onPress={() => state ?.setSelectedKeys( new Set() )} > Clear </Button> ); } <ToggleButtonGroup selectionMode="multiple" defaultSelectedKeys={[ 'bold', 'italic' ]} > <ToggleButton id="bold"> Bold </ToggleButton> <ToggleButton id="italic"> Italic </ToggleButton> <ToggleButton id="underline"> Underline </ToggleButton> <ClearButton /></ToggleButtonGroup> BoldItalicUnderlineClear ### Hooks# If you need to customize things even further, such as accessing internal state, intercepting events, or customizing the DOM structure, you can drop down to the lower level Hook-based API. See useToggleButtonGroup for more details. --- ## Page: https://react-spectrum.adobe.com/react-aria/GridList.html # GridList A grid list displays a list of interactive items, with support for keyboard navigation, single or multiple selection, and row actions. <table><tbody><tr><th>install</th><td><code>yarn add react-aria-components</code></td></tr><tr><th>version</th><td>1.10.1</td></tr><tr><th>usage</th><td><code><span>import</span> {GridList} <span>from</span> <span>'react-aria-components'</span></code></td></tr></tbody></table> View ARIA pattern W3C View repository GitHub View package NPM ## Example# * * * import {GridList, GridListItem, Button} from 'react-aria-components'; import {MyCheckbox} from './Checkbox'; <GridList aria-label="Favorite pokemon" selectionMode="multiple"> <GridListItem textValue="Charizard"> <MyCheckbox slot="selection" /> Charizard <Button aria-label="Info">ⓘ</Button> </GridListItem> <GridListItem textValue="Blastoise"> <MyCheckbox slot="selection" /> Blastoise <Button aria-label="Info">ⓘ</Button> </GridListItem> <GridListItem textValue="Venusaur"> <MyCheckbox slot="selection" /> Venusaur <Button aria-label="Info">ⓘ</Button> </GridListItem> <GridListItem textValue="Pikachu"> <MyCheckbox slot="selection" /> Pikachu <Button aria-label="Info">ⓘ</Button> </GridListItem> </GridList> import { Button, GridList, GridListItem } from 'react-aria-components'; import {MyCheckbox} from './Checkbox'; <GridList aria-label="Favorite pokemon" selectionMode="multiple" > <GridListItem textValue="Charizard"> <MyCheckbox slot="selection" /> Charizard <Button aria-label="Info">ⓘ</Button> </GridListItem> <GridListItem textValue="Blastoise"> <MyCheckbox slot="selection" /> Blastoise <Button aria-label="Info">ⓘ</Button> </GridListItem> <GridListItem textValue="Venusaur"> <MyCheckbox slot="selection" /> Venusaur <Button aria-label="Info">ⓘ</Button> </GridListItem> <GridListItem textValue="Pikachu"> <MyCheckbox slot="selection" /> Pikachu <Button aria-label="Info">ⓘ</Button> </GridListItem> </GridList> import { Button, GridList, GridListItem } from 'react-aria-components'; import {MyCheckbox} from './Checkbox'; <GridList aria-label="Favorite pokemon" selectionMode="multiple" > <GridListItem textValue="Charizard"> <MyCheckbox slot="selection" /> Charizard <Button aria-label="Info"> ⓘ </Button> </GridListItem> <GridListItem textValue="Blastoise"> <MyCheckbox slot="selection" /> Blastoise <Button aria-label="Info"> ⓘ </Button> </GridListItem> <GridListItem textValue="Venusaur"> <MyCheckbox slot="selection" /> Venusaur <Button aria-label="Info"> ⓘ </Button> </GridListItem> <GridListItem textValue="Pikachu"> <MyCheckbox slot="selection" /> Pikachu <Button aria-label="Info"> ⓘ </Button> </GridListItem> </GridList> Charizardⓘ Blastoiseⓘ Venusaurⓘ Pikachuⓘ Show CSS @import "@react-aria/example-theme"; .react-aria-GridList { display: flex; flex-direction: column; gap: 2px; max-height: inherit; overflow: auto; padding: 4px; border: 1px solid var(--border-color); border-radius: 6px; background: var(--overlay-background); forced-color-adjust: none; outline: none; width: 250px; max-height: 300px; min-height: 100px; box-sizing: border-box; &[data-focus-visible] { outline: 2px solid var(--focus-ring-color); outline-offset: -1px; } .react-aria-GridListItem { display: flex; align-items: center; gap: 0.571rem; min-height: 28px; padding: 0.286rem 0.286rem 0.286rem 0.571rem; border-radius: 6px; outline: none; cursor: default; color: var(--text-color); font-size: 1.072rem; position: relative; transform: translateZ(0); &[data-focus-visible] { outline: 2px solid var(--focus-ring-color); outline-offset: -2px; } &[data-pressed] { background: var(--gray-100); } &[data-selected] { background: var(--highlight-background); color: var(--highlight-foreground); --focus-ring-color: var(--highlight-foreground); &[data-focus-visible] { outline-color: var(--highlight-foreground); outline-offset: -4px; } .react-aria-Button { color: var(--highlight-foreground); --highlight-hover: rgb(255 255 255 / 0.1); --highlight-pressed: rgb(255 255 255 / 0.2); } } &[data-disabled] { color: var(--text-color-disabled); } .react-aria-Button:not([slot]) { margin-left: auto; } .react-aria-Button { background: transparent; border: none; font-size: 1.2rem; line-height: 1.2em; padding: 0.286rem 0.429rem; transition: background 200ms; &[data-hovered] { background: var(--highlight-hover); } &[data-pressed] { background: var(--highlight-pressed); box-shadow: none; } } } /* join selected items if :has selector is supported */ @supports selector(:has(.foo)) { gap: 0; .react-aria-GridListItem[data-selected]:has(+ [data-selected]), .react-aria-GridListItem[data-selected]:has(+ .react-aria-DropIndicator + [data-selected]) { border-end-start-radius: 0; border-end-end-radius: 0; } .react-aria-GridListItem[data-selected] + [data-selected], .react-aria-GridListItem[data-selected] + .react-aria-DropIndicator + [data-selected] { border-start-start-radius: 0; border-start-end-radius: 0; } } :where(.react-aria-GridListItem) .react-aria-Checkbox { --selected-color: var(--highlight-foreground); --selected-color-pressed: var(--highlight-foreground-pressed); --checkmark-color: var(--highlight-background); --background-color: var(--highlight-background); } } @import "@react-aria/example-theme"; .react-aria-GridList { display: flex; flex-direction: column; gap: 2px; max-height: inherit; overflow: auto; padding: 4px; border: 1px solid var(--border-color); border-radius: 6px; background: var(--overlay-background); forced-color-adjust: none; outline: none; width: 250px; max-height: 300px; min-height: 100px; box-sizing: border-box; &[data-focus-visible] { outline: 2px solid var(--focus-ring-color); outline-offset: -1px; } .react-aria-GridListItem { display: flex; align-items: center; gap: 0.571rem; min-height: 28px; padding: 0.286rem 0.286rem 0.286rem 0.571rem; border-radius: 6px; outline: none; cursor: default; color: var(--text-color); font-size: 1.072rem; position: relative; transform: translateZ(0); &[data-focus-visible] { outline: 2px solid var(--focus-ring-color); outline-offset: -2px; } &[data-pressed] { background: var(--gray-100); } &[data-selected] { background: var(--highlight-background); color: var(--highlight-foreground); --focus-ring-color: var(--highlight-foreground); &[data-focus-visible] { outline-color: var(--highlight-foreground); outline-offset: -4px; } .react-aria-Button { color: var(--highlight-foreground); --highlight-hover: rgb(255 255 255 / 0.1); --highlight-pressed: rgb(255 255 255 / 0.2); } } &[data-disabled] { color: var(--text-color-disabled); } .react-aria-Button:not([slot]) { margin-left: auto; } .react-aria-Button { background: transparent; border: none; font-size: 1.2rem; line-height: 1.2em; padding: 0.286rem 0.429rem; transition: background 200ms; &[data-hovered] { background: var(--highlight-hover); } &[data-pressed] { background: var(--highlight-pressed); box-shadow: none; } } } /* join selected items if :has selector is supported */ @supports selector(:has(.foo)) { gap: 0; .react-aria-GridListItem[data-selected]:has(+ [data-selected]), .react-aria-GridListItem[data-selected]:has(+ .react-aria-DropIndicator + [data-selected]) { border-end-start-radius: 0; border-end-end-radius: 0; } .react-aria-GridListItem[data-selected] + [data-selected], .react-aria-GridListItem[data-selected] + .react-aria-DropIndicator + [data-selected] { border-start-start-radius: 0; border-start-end-radius: 0; } } :where(.react-aria-GridListItem) .react-aria-Checkbox { --selected-color: var(--highlight-foreground); --selected-color-pressed: var(--highlight-foreground-pressed); --checkmark-color: var(--highlight-background); --background-color: var(--highlight-background); } } @import "@react-aria/example-theme"; .react-aria-GridList { display: flex; flex-direction: column; gap: 2px; max-height: inherit; overflow: auto; padding: 4px; border: 1px solid var(--border-color); border-radius: 6px; background: var(--overlay-background); forced-color-adjust: none; outline: none; width: 250px; max-height: 300px; min-height: 100px; box-sizing: border-box; &[data-focus-visible] { outline: 2px solid var(--focus-ring-color); outline-offset: -1px; } .react-aria-GridListItem { display: flex; align-items: center; gap: 0.571rem; min-height: 28px; padding: 0.286rem 0.286rem 0.286rem 0.571rem; border-radius: 6px; outline: none; cursor: default; color: var(--text-color); font-size: 1.072rem; position: relative; transform: translateZ(0); &[data-focus-visible] { outline: 2px solid var(--focus-ring-color); outline-offset: -2px; } &[data-pressed] { background: var(--gray-100); } &[data-selected] { background: var(--highlight-background); color: var(--highlight-foreground); --focus-ring-color: var(--highlight-foreground); &[data-focus-visible] { outline-color: var(--highlight-foreground); outline-offset: -4px; } .react-aria-Button { color: var(--highlight-foreground); --highlight-hover: rgb(255 255 255 / 0.1); --highlight-pressed: rgb(255 255 255 / 0.2); } } &[data-disabled] { color: var(--text-color-disabled); } .react-aria-Button:not([slot]) { margin-left: auto; } .react-aria-Button { background: transparent; border: none; font-size: 1.2rem; line-height: 1.2em; padding: 0.286rem 0.429rem; transition: background 200ms; &[data-hovered] { background: var(--highlight-hover); } &[data-pressed] { background: var(--highlight-pressed); box-shadow: none; } } } /* join selected items if :has selector is supported */ @supports selector(:has(.foo)) { gap: 0; .react-aria-GridListItem[data-selected]:has(+ [data-selected]), .react-aria-GridListItem[data-selected]:has(+ .react-aria-DropIndicator + [data-selected]) { border-end-start-radius: 0; border-end-end-radius: 0; } .react-aria-GridListItem[data-selected] + [data-selected], .react-aria-GridListItem[data-selected] + .react-aria-DropIndicator + [data-selected] { border-start-start-radius: 0; border-start-end-radius: 0; } } :where(.react-aria-GridListItem) .react-aria-Checkbox { --selected-color: var(--highlight-foreground); --selected-color-pressed: var(--highlight-foreground-pressed); --checkmark-color: var(--highlight-background); --background-color: var(--highlight-background); } } ## Features# * * * A list can be built using <ul> or <ol> HTML elements, but does not support any user interactions. HTML lists are meant for static content, rather than lists with rich interactions like focusable elements within rows, keyboard navigation, row selection, etc. `GridList` helps achieve accessible and interactive list components that can be styled as needed. * **Item selection** – Single or multiple selection, with optional checkboxes, disabled rows, and both `toggle` and `replace` selection behaviors. * **Interactive children** – List items may include interactive elements such as buttons, checkboxes, menus, etc. * **Actions** – Items support optional row actions such as navigation via click, tap, double click, or Enter key. * **Async loading** – Support for loading items asynchronously. * **Keyboard navigation** – List items and focusable children can be navigated using the arrow keys, along with page up/down, home/end, etc. Typeahead, auto scrolling, and selection modifier keys are supported as well. * **Drag and drop** – GridList supports drag and drop to reorder, insert, or update items via mouse, touch, keyboard, and screen reader interactions. * **Virtualized scrolling** – Use Virtualizer to improve performance of large lists by rendering only the visible items. * **Touch friendly** – Selection and actions adapt their behavior depending on the device. For example, selection is activated via long press on touch when item actions are present. * **Accessible** – Follows the ARIA grid pattern, with additional selection announcements via an ARIA live region. Extensively tested across many devices and assistive technologies to ensure announcements and behaviors are consistent. **Note**: Use `GridList` when your list items may contain interactive elements such as buttons, checkboxes, menus, etc. within them. If your list items contain only static content such as text and images, then consider using ListBox instead for a slightly better screen reader experience (especially on mobile). ## Anatomy# * * * A grid list consists of a container element, with rows of data inside. The rows within a list may contain focusable elements or plain text content. If the list supports row selection, each row can optionally include a selection checkbox. import {GridList, GridListItem, Checkbox, Button} from 'react-aria-components'; <GridList> <GridListItem> <Button slot="drag" /> <Checkbox slot="selection" /> </GridListItem> </GridList> import { Button, Checkbox, GridList, GridListItem } from 'react-aria-components'; <GridList> <GridListItem> <Button slot="drag" /> <Checkbox slot="selection" /> </GridListItem> </GridList> import { Button, Checkbox, GridList, GridListItem } from 'react-aria-components'; <GridList> <GridListItem> <Button slot="drag" /> <Checkbox slot="selection" /> </GridListItem> </GridList> ### Concepts# `GridList` makes use of the following concepts: Collections Defining collections of items, async loading, and updating items over time. Selection Interactions and data structures to represent selection. Drag and drop Concepts and interactions for an accessible drag and drop experience. ### Composed components# A `GridList` uses the following components, which may also be used standalone or reused in other components. Checkbox A checkbox allows a user to select an individual option. Button A button allows a user to perform an action. ## Examples# * * * iOS List View A GridList with swipe gestures, layout animations, and multi selection. ## Starter kits# * * * To help kick-start your project, we offer starter kits that include example implementations of all React Aria components with various styling solutions. All components are fully styled, including support for dark mode, high contrast mode, and all UI states. Each starter comes with a pre-configured Storybook that you can experiment with, or use as a starting point for your own component library. Vanilla CSS Download ZIP Preview Tailwind CSS Download ZIP Preview ## Reusable wrappers# * * * If you will use a GridList in multiple places in your app, you can wrap all of the pieces into a reusable component. This way, the DOM structure, styling code, and other logic are defined in a single place and reused everywhere to ensure consistency. This example wraps `GridList` and all of its children together into a single component which accepts a `label` prop and `children`, which are passed through to the right places. The `GridListItem` component is also wrapped to include a custom checkbox component automatically when the item is multi-selectable, and a drag handle when drag and drop is enabled. import type {GridListItemProps, GridListProps} from 'react-aria-components'; export function MyGridList<T extends object>( { children, ...props }: GridListProps<T> ) { return ( <GridList {...props}> {children} </GridList> ); } export function MyItem( { children, ...props }: Omit<GridListItemProps, 'children'> & { children?: React.ReactNode; } ) { let textValue = typeof children === 'string' ? children : undefined; return ( <GridListItem textValue={textValue} {...props}> {({ selectionMode, selectionBehavior, allowsDragging }) => ( <> {/* Add elements for drag and drop and selection. */} {allowsDragging && <Button slot="drag">≡</Button>} {selectionMode === 'multiple' && selectionBehavior === 'toggle' && ( <MyCheckbox slot="selection" /> )} {children} </> )} </GridListItem> ); } <MyGridList aria-label="Ice cream flavors" selectionMode="multiple"> <MyItem>Chocolate</MyItem> <MyItem>Mint</MyItem> <MyItem>Strawberry</MyItem> <MyItem>Vanilla</MyItem> </MyGridList> import type { GridListItemProps, GridListProps } from 'react-aria-components'; export function MyGridList<T extends object>( { children, ...props }: GridListProps<T> ) { return ( <GridList {...props}> {children} </GridList> ); } export function MyItem( { children, ...props }: & Omit<GridListItemProps, 'children'> & { children?: React.ReactNode } ) { let textValue = typeof children === 'string' ? children : undefined; return ( <GridListItem textValue={textValue} {...props}> {( { selectionMode, selectionBehavior, allowsDragging } ) => ( <> {/* Add elements for drag and drop and selection. */} {allowsDragging && <Button slot="drag">≡</Button>} {selectionMode === 'multiple' && selectionBehavior === 'toggle' && ( <MyCheckbox slot="selection" /> )} {children} </> )} </GridListItem> ); } <MyGridList aria-label="Ice cream flavors" selectionMode="multiple" > <MyItem>Chocolate</MyItem> <MyItem>Mint</MyItem> <MyItem>Strawberry</MyItem> <MyItem>Vanilla</MyItem> </MyGridList> import type { GridListItemProps, GridListProps } from 'react-aria-components'; export function MyGridList< T extends object >( { children, ...props }: GridListProps<T> ) { return ( <GridList {...props}> {children} </GridList> ); } export function MyItem({ children, ...props }: & Omit< GridListItemProps, 'children' > & { children?: React.ReactNode; }) { let textValue = typeof children === 'string' ? children : undefined; return ( <GridListItem textValue={textValue} {...props} > {( { selectionMode, selectionBehavior, allowsDragging } ) => ( <> {/* Add elements for drag and drop and selection. */} {allowsDragging && ( <Button slot="drag"> ≡ </Button> )} {selectionMode === 'multiple' && selectionBehavior === 'toggle' && ( <MyCheckbox slot="selection" /> )} {children} </> )} </GridListItem> ); } <MyGridList aria-label="Ice cream flavors" selectionMode="multiple" > <MyItem> Chocolate </MyItem> <MyItem>Mint</MyItem> <MyItem> Strawberry </MyItem> <MyItem> Vanilla </MyItem> </MyGridList> Chocolate Mint Strawberry Vanilla ## Content# * * * So far, our examples have shown static collections, where the data is hard coded. Dynamic collections, as shown below, can be used when the data comes from an external data source such as an API, or updates over time. In the example below, the rows are provided to the GridList via a render function. interface ItemValue { id: number; name: string; } function ExampleList(props: GridListProps<ItemValue>) { let rows = [ { id: 1, name: 'Games' }, { id: 2, name: 'Program Files' }, { id: 3, name: 'bootmgr' }, { id: 4, name: 'log.txt' } ]; return ( <MyGridList aria-label="Example dynamic collection List" selectionMode="multiple" items={rows} ...props} > {(item) => ( <MyItem textValue={item.name}> {item.name} <Button aria-label="Info" onPress={() => alert(`Info for ${item.name}...`)} > ⓘ </Button> </MyItem> )} </MyGridList> ); } interface ItemValue { id: number; name: string; } function ExampleList(props: GridListProps<ItemValue>) { let rows = [ { id: 1, name: 'Games' }, { id: 2, name: 'Program Files' }, { id: 3, name: 'bootmgr' }, { id: 4, name: 'log.txt' } ]; return ( <MyGridList aria-label="Example dynamic collection List" selectionMode="multiple" items={rows} ...props} > {(item) => ( <MyItem textValue={item.name}> {item.name} <Button aria-label="Info" onPress={() => alert(`Info for ${item.name}...`)} > ⓘ </Button> </MyItem> )} </MyGridList> ); } interface ItemValue { id: number; name: string; } function ExampleList( props: GridListProps< ItemValue > ) { let rows = [ { id: 1, name: 'Games' }, { id: 2, name: 'Program Files' }, { id: 3, name: 'bootmgr' }, { id: 4, name: 'log.txt' } ]; return ( <MyGridList aria-label="Example dynamic collection List" selectionMode="multiple" items={rows} ...props} > {(item) => ( <MyItem textValue={item .name} > {item.name} <Button aria-label="Info" onPress={() => alert( `Info for ${item.name}...` )} > ⓘ </Button> </MyItem> )} </MyGridList> ); } Gamesⓘ Program Filesⓘ bootmgrⓘ log.txtⓘ ## Selection# * * * ### Single selection# By default, `GridList` doesn't allow row selection but this can be enabled using the `selectionMode` prop. Use `defaultSelectedKeys` to provide a default set of selected rows. Note that the value of the selected keys must match the `id` prop of the row. The example below enables single selection mode, and uses `defaultSelectedKeys` to select the id with key equal to `2`. A user can click on a different row to change the selection, or click on the same row again to deselect it entirely. // Using the example above <ExampleList aria-label="List with single selection" selectionMode="single" defaultSelectedKeys={[2]}/> // Using the example above <ExampleList aria-label="List with single selection" selectionMode="single" defaultSelectedKeys={[2]}/> // Using the example above <ExampleList aria-label="List with single selection" selectionMode="single" defaultSelectedKeys={[ 2 ]}/> Gamesⓘ Program Filesⓘ bootmgrⓘ log.txtⓘ ### Multiple selection# Multiple selection can be enabled by setting `selectionMode` to `multiple`. Our example displays checkboxes when the list allows multiple selection. <ExampleList aria-label="List with multiple selection" selectionMode="multiple" defaultSelectedKeys={[2, 4]} /> <ExampleList aria-label="List with multiple selection" selectionMode="multiple" defaultSelectedKeys={[2, 4]} /> <ExampleList aria-label="List with multiple selection" selectionMode="multiple" defaultSelectedKeys={[ 2, 4 ]} /> Gamesⓘ Program Filesⓘ bootmgrⓘ log.txtⓘ ### Disallow empty selection# `GridList` also supports a `disallowEmptySelection` prop which forces the user to have at least one row in the List selected at all times. In this mode, if a single row is selected and the user presses it, it will not be deselected. <ExampleList aria-label="List with disallowed empty selection" selectionMode="multiple" defaultSelectedKeys={[2]} disallowEmptySelection/> <ExampleList aria-label="List with disallowed empty selection" selectionMode="multiple" defaultSelectedKeys={[2]} disallowEmptySelection/> <ExampleList aria-label="List with disallowed empty selection" selectionMode="multiple" defaultSelectedKeys={[ 2 ]} disallowEmptySelection/> Gamesⓘ Program Filesⓘ bootmgrⓘ log.txtⓘ ### Controlled selection# To programmatically control row selection, use the `selectedKeys` prop paired with the `onSelectionChange` callback. The `id` prop from the selected rows will be passed into the callback when the row is pressed, allowing you to update state accordingly. import type {Selection} from 'react-aria-components'; function PokemonList(props: GridListProps<ItemValue>) { let rows = [ {id: 1, name: 'Charizard'}, {id: 2, name: 'Blastoise'}, {id: 3, name: 'Venusaur'}, {id: 4, name: 'Pikachu'} ]; let [selectedKeys, setSelectedKeys] = React.useState<Selection>(new Set([2])); return ( <MyGridList aria-label="List with controlled selection" items={rows} selectionMode="multiple" selectedKeys={selectedKeys} onSelectionChange={setSelectedKeys} {...props}> {item => <MyItem>{item.name}</MyItem>} </MyGridList> ); } import type {Selection} from 'react-aria-components'; function PokemonList(props: GridListProps<ItemValue>) { let rows = [ { id: 1, name: 'Charizard' }, { id: 2, name: 'Blastoise' }, { id: 3, name: 'Venusaur' }, { id: 4, name: 'Pikachu' } ]; let [selectedKeys, setSelectedKeys] = React.useState< Selection >(new Set([2])); return ( <MyGridList aria-label="List with controlled selection" items={rows} selectionMode="multiple" selectedKeys={selectedKeys} onSelectionChange={setSelectedKeys} ...props} > {(item) => <MyItem>{item.name}</MyItem>} </MyGridList> ); } import type {Selection} from 'react-aria-components'; function PokemonList( props: GridListProps< ItemValue > ) { let rows = [ { id: 1, name: 'Charizard' }, { id: 2, name: 'Blastoise' }, { id: 3, name: 'Venusaur' }, { id: 4, name: 'Pikachu' } ]; let [ selectedKeys, setSelectedKeys ] = React.useState< Selection >(new Set([2])); return ( <MyGridList aria-label="List with controlled selection" items={rows} selectionMode="multiple" selectedKeys={selectedKeys} onSelectionChange={setSelectedKeys} ...props} > {(item) => ( <MyItem> {item.name} </MyItem> )} </MyGridList> ); } Charizard Blastoise Venusaur Pikachu ### Selection behavior# By default, `GridList` uses the `"toggle"` selection behavior, which behaves like a checkbox group: clicking, tapping, or pressing the Space or Enter keys toggles selection for the focused row. Using the arrow keys moves focus but does not change selection. The `"toggle"` selection mode is often paired with a checkbox in each row as an explicit affordance for selection. When `selectionBehavior` is set to `"replace"`, clicking a row with the mouse replaces the selection with only that row. Using the arrow keys moves both focus and selection. To select multiple rows, modifier keys such as Ctrl, Cmd, and Shift can be used. On touch screen devices, selection always behaves as toggle since modifier keys may not be available. These selection styles implement the behaviors defined in Aria Practices. <PokemonList aria-label="List with replace selection behavior" selectionMode="multiple" selectionBehavior="replace"/> <PokemonList aria-label="List with replace selection behavior" selectionMode="multiple" selectionBehavior="replace"/> <PokemonList aria-label="List with replace selection behavior" selectionMode="multiple" selectionBehavior="replace"/> Charizard Blastoise Venusaur Pikachu ## Row actions# * * * `GridList` supports row actions via the `onAction` prop, which is useful for functionality such as navigation. When nothing is selected, the list performs actions by default when clicking or tapping a row. Items may be selected using the checkbox, or by long pressing on touch devices. When at least one item is selected, the list is in selection mode, and clicking or tapping a row toggles the selection. Actions may also be triggered via the Enter key, and selection using the Space key. This behavior is slightly different when `selectionBehavior="replace"`, where single clicking selects the row and actions are performed via double click. Touch and keyboard behaviors are unaffected. <div style={{ display: 'flex', flexWrap: 'wrap', gap: 24 }}> <ExampleList aria-label="Checkbox selection list with row actions" selectionMode="multiple" selectionBehavior="toggle" onAction={key => alert(`Opening item ${key}...`)} /> <ExampleList aria-label="Highlight selection list with row actions" selectionMode="multiple" selectionBehavior="replace" onAction={key => alert(`Opening item ${key}...`)} /> </div> <div style={{ display: 'flex', flexWrap: 'wrap', gap: 24 }}> <ExampleList aria-label="Checkbox selection list with row actions" selectionMode="multiple" selectionBehavior="toggle" onAction={key => alert(`Opening item ${key}...`)} /> <ExampleList aria-label="Highlight selection list with row actions" selectionMode="multiple" selectionBehavior="replace" onAction={key => alert(`Opening item ${key}...`)} /> </div> <div style={{ display: 'flex', flexWrap: 'wrap', gap: 24 }} > <ExampleList aria-label="Checkbox selection list with row actions" selectionMode="multiple" selectionBehavior="toggle" onAction={(key) => alert( `Opening item ${key}...` )} /> <ExampleList aria-label="Highlight selection list with row actions" selectionMode="multiple" selectionBehavior="replace" onAction={(key) => alert( `Opening item ${key}...` )} /> </div> Gamesⓘ Program Filesⓘ bootmgrⓘ log.txtⓘ Gamesⓘ Program Filesⓘ bootmgrⓘ log.txtⓘ Rows may also have a row action specified by directly applying `onAction` on the `GridListItem` itself. This may be especially convenient in static collections. If `onAction` is also provided to the `GridList`, both the grid list's and the row's `onAction` are called. <MyGridList aria-label="List with onAction applied on the rows directly" selectionMode="multiple" > <MyItem onAction={() => alert(`Opening Games`)}> Games </MyItem> <MyItem onAction={() => alert(`Opening Program Files`)}> Program Files </MyItem> <MyItem onAction={() => alert(`Opening bootmgr`)}> bootmgr </MyItem> <MyItem onAction={() => alert(`Opening log.txt`)}> log.txt </MyItem> </MyGridList> <MyGridList aria-label="List with onAction applied on the rows directly" selectionMode="multiple" > <MyItem onAction={() => alert(`Opening Games`)}> Games </MyItem> <MyItem onAction={() => alert(`Opening Program Files`)}> Program Files </MyItem> <MyItem onAction={() => alert(`Opening bootmgr`)}> bootmgr </MyItem> <MyItem onAction={() => alert(`Opening log.txt`)}> log.txt </MyItem> </MyGridList> <MyGridList aria-label="List with onAction applied on the rows directly" selectionMode="multiple" > <MyItem onAction={() => alert( `Opening Games` )} > Games </MyItem> <MyItem onAction={() => alert( `Opening Program Files` )} > Program Files </MyItem> <MyItem onAction={() => alert( `Opening bootmgr` )} > bootmgr </MyItem> <MyItem onAction={() => alert( `Opening log.txt` )} > log.txt </MyItem> </MyGridList> Games Program Files bootmgr log.txt ### Links# Items in a GridList may also be links to another page or website. This can be achieved by passing the `href` prop to the `<GridListItem>` component. Links behave the same way as described above for row actions depending on the `selectionMode` and `selectionBehavior`. <MyGridList aria-label="Links" selectionMode="multiple"> <MyItem href="https://adobe.com/" target="_blank">Adobe</MyItem> <MyItem href="https://apple.com/" target="_blank">Apple</MyItem> <MyItem href="https://google.com/" target="_blank">Google</MyItem> <MyItem href="https://microsoft.com/" target="_blank">Microsoft</MyItem> </MyGridList> <MyGridList aria-label="Links" selectionMode="multiple"> <MyItem href="https://adobe.com/" target="_blank"> Adobe </MyItem> <MyItem href="https://apple.com/" target="_blank"> Apple </MyItem> <MyItem href="https://google.com/" target="_blank"> Google </MyItem> <MyItem href="https://microsoft.com/" target="_blank"> Microsoft </MyItem> </MyGridList> <MyGridList aria-label="Links" selectionMode="multiple" > <MyItem href="https://adobe.com/" target="_blank" > Adobe </MyItem> <MyItem href="https://apple.com/" target="_blank" > Apple </MyItem> <MyItem href="https://google.com/" target="_blank" > Google </MyItem> <MyItem href="https://microsoft.com/" target="_blank" > Microsoft </MyItem> </MyGridList> Adobe Apple Google Microsoft #### Client side routing# The `<GridListItem>` component works with frameworks and client side routers like Next.js and React Router. As with other React Aria components that support links, this works via the `RouterProvider` component at the root of your app. See the client side routing guide to learn how to set this up. ## Disabled items# * * * A `GridListItem` can be disabled with the `isDisabled` prop. This will disable all interactions on the row, unless the `disabledBehavior` prop on `GridList` is used to change this behavior. Note that you are responsible for the styling of disabled rows, however, the selection checkbox will be automatically disabled. <MyGridList aria-label="List with disabled rows" selectionMode="multiple"> <MyItem>Charizard</MyItem> <MyItem>Blastoise</MyItem> <MyItem isDisabled>Venusaur</MyItem> <MyItem>Pikachu</MyItem> </MyGridList> <MyGridList aria-label="List with disabled rows" selectionMode="multiple" > <MyItem>Charizard</MyItem> <MyItem>Blastoise</MyItem> <MyItem isDisabled>Venusaur</MyItem> <MyItem>Pikachu</MyItem> </MyGridList> <MyGridList aria-label="List with disabled rows" selectionMode="multiple" > <MyItem> Charizard </MyItem> <MyItem> Blastoise </MyItem> <MyItem isDisabled> Venusaur </MyItem> <MyItem> Pikachu </MyItem> </MyGridList> Charizard Blastoise Venusaur Pikachu When `disabledBehavior` is set to `selection`, interactions such as focus, dragging, or actions can still be performed on disabled rows. <MyGridList aria-label="List with disabled rows" selectionMode="multiple" disabledBehavior="selection"> <MyItem>Charizard</MyItem> <MyItem>Blastoise</MyItem> <MyItem isDisabled>Venusaur</MyItem> <MyItem>Pikachu</MyItem> </MyGridList> <MyGridList aria-label="List with disabled rows" selectionMode="multiple" disabledBehavior="selection"> <MyItem>Charizard</MyItem> <MyItem>Blastoise</MyItem> <MyItem isDisabled>Venusaur</MyItem> <MyItem>Pikachu</MyItem> </MyGridList> <MyGridList aria-label="List with disabled rows" selectionMode="multiple" disabledBehavior="selection"> <MyItem> Charizard </MyItem> <MyItem> Blastoise </MyItem> <MyItem isDisabled> Venusaur </MyItem> <MyItem> Pikachu </MyItem> </MyGridList> Charizard Blastoise Venusaur Pikachu In dynamic collections, it may be more convenient to use the `disabledKeys` prop at the `GridList` level instead of `isDisabled` on individual items. This accepts a list of item ids that are disabled. An item is considered disabled if its key exists in `disabledKeys` or if it has `isDisabled`. // Using the example above <PokemonList aria-label="List with disabled rows" selectionMode="multiple" disabledKeys={[3]}/> // Using the example above <PokemonList aria-label="List with disabled rows" selectionMode="multiple" disabledKeys={[3]}/> // Using the example above <PokemonList aria-label="List with disabled rows" selectionMode="multiple" disabledKeys={[3]}/> Charizard Blastoise Venusaur Pikachu ## Asynchronous loading# * * * This example uses the useAsyncList hook to handle asynchronous loading of data from a server. You may additionally want to display a spinner to indicate the loading state to the user, or support features like infinite scroll to load more data. import {useAsyncList} from 'react-stately'; interface Character { name: string; } function AsyncList() { let list = useAsyncList<Character>({ async load({ signal, cursor }) { if (cursor) { cursor = cursor.replace(/^http:\/\//i, 'https://'); } let res = await fetch( cursor || `https://swapi.py4e.com/api/people/?search=`, { signal } ); let json = await res.json(); return { items: json.results, cursor: json.next }; } }); return ( <MyGridList selectionMode="multiple" aria-label="Async loading ListView example" items={list.items} > {(item) => <MyItem id={item.name}>{item.name}</MyItem>} </MyGridList> ); } import {useAsyncList} from 'react-stately'; interface Character { name: string; } function AsyncList() { let list = useAsyncList<Character>({ async load({ signal, cursor }) { if (cursor) { cursor = cursor.replace(/^http:\/\//i, 'https://'); } let res = await fetch( cursor || `https://swapi.py4e.com/api/people/?search=`, { signal } ); let json = await res.json(); return { items: json.results, cursor: json.next }; } }); return ( <MyGridList selectionMode="multiple" aria-label="Async loading ListView example" items={list.items} > {(item) => <MyItem id={item.name}>{item.name} </MyItem>} </MyGridList> ); } import {useAsyncList} from 'react-stately'; interface Character { name: string; } function AsyncList() { let list = useAsyncList< Character >({ async load( { signal, cursor } ) { if (cursor) { cursor = cursor .replace( /^http:\/\//i, 'https://' ); } let res = await fetch( cursor || `https://swapi.py4e.com/api/people/?search=`, { signal } ); let json = await res .json(); return { items: json.results, cursor: json.next }; } }); return ( <MyGridList selectionMode="multiple" aria-label="Async loading ListView example" items={list.items} > {(item) => ( <MyItem id={item.name} > {item.name} </MyItem> )} </MyGridList> ); } Luke Skywalker C-3PO R2-D2 Darth Vader Leia Organa Owen Lars Beru Whitesun lars R5-D4 Biggs Darklighter Obi-Wan Kenobi ## Empty state# * * * Use the `renderEmptyState` prop to customize what the `GridList` will display if there are no items. <GridList aria-label="Search results" renderEmptyState={() => 'No results found.'}> {[]} </GridList> <GridList aria-label="Search results" renderEmptyState={() => 'No results found.'}> {[]} </GridList> <GridList aria-label="Search results" renderEmptyState={() => 'No results found.'}> {[]} </GridList> No results found. Show CSS .react-aria-GridList { &[data-empty] { align-items: center; justify-content: center; font-style: italic; } } .react-aria-GridList { &[data-empty] { align-items: center; justify-content: center; font-style: italic; } } .react-aria-GridList { &[data-empty] { align-items: center; justify-content: center; font-style: italic; } } ## Drag and drop# * * * GridList supports drag and drop interactions when the `dragAndDropHooks` prop is provided using the `useDragAndDrop` hook. Users can drop data on the list as a whole, on individual items, insert new items between existing ones, or reorder items. React Aria supports traditional mouse and touch based drag and drop, but also implements keyboard and screen reader friendly interactions. Users can press Enter on a draggable element to enter drag and drop mode. Then, they can press Tab to navigate between drop targets. A droppable collection is treated as a single drop target, so that users can easily tab past it to get to the next drop target. Within a droppable collection, keys such as ArrowDown and ArrowUp can be used to select a _drop position_, such as on an item, or between items. Draggable items must include a focusable drag handle using a `<Button slot="drag">`. This enables keyboard and screen reader users to initiate drag and drop. The `MyItem` component defined in the reusable wrappers section above includes this automatically when the list allows dragging. See the drag and drop introduction to learn more. ### Reorderable# This example shows a basic list that allows users to reorder items via drag and drop. This is enabled using the `onReorder` event handler, provided to the `useDragAndDrop` hook. The `getItems` function must also be implemented for items to become draggable. See below for more details. This uses useListData from React Stately to manage the item list. Note that `useListData` is a convenience hook, not a requirement. You can manage your state however you wish. import {useListData} from 'react-stately'; import {useDragAndDrop} from 'react-aria-components'; function Example() { let list = useListData({ initialItems: [ { id: 1, name: 'Adobe Photoshop' }, { id: 2, name: 'Adobe XD' }, { id: 3, name: 'Adobe Dreamweaver' }, { id: 4, name: 'Adobe InDesign' }, { id: 5, name: 'Adobe Connect' } ] }); let { dragAndDropHooks } = useDragAndDrop({ getItems: (keys) => [...keys].map((key) => ({ 'text/plain': list.getItem(key).name })), onReorder(e) { if (e.target.dropPosition === 'before') { list.moveBefore(e.target.key, e.keys); } else if (e.target.dropPosition === 'after') { list.moveAfter(e.target.key, e.keys); } } }); return ( <MyGridList aria-label="Reorderable list" selectionMode="multiple" items={list.items} dragAndDropHooks={dragAndDropHooks} > {(item) => <MyItem>{item.name}</MyItem>} </MyGridList> ); } import {useListData} from 'react-stately'; import {useDragAndDrop} from 'react-aria-components'; function Example() { let list = useListData({ initialItems: [ { id: 1, name: 'Adobe Photoshop' }, { id: 2, name: 'Adobe XD' }, { id: 3, name: 'Adobe Dreamweaver' }, { id: 4, name: 'Adobe InDesign' }, { id: 5, name: 'Adobe Connect' } ] }); let { dragAndDropHooks } = useDragAndDrop({ getItems: (keys) => [...keys].map((key) => ({ 'text/plain': list.getItem(key).name })), onReorder(e) { if (e.target.dropPosition === 'before') { list.moveBefore(e.target.key, e.keys); } else if (e.target.dropPosition === 'after') { list.moveAfter(e.target.key, e.keys); } } }); return ( <MyGridList aria-label="Reorderable list" selectionMode="multiple" items={list.items} dragAndDropHooks={dragAndDropHooks} > {(item) => <MyItem>{item.name}</MyItem>} </MyGridList> ); } import {useListData} from 'react-stately'; import {useDragAndDrop} from 'react-aria-components'; function Example() { let list = useListData( { initialItems: [ { id: 1, name: 'Adobe Photoshop' }, { id: 2, name: 'Adobe XD' }, { id: 3, name: 'Adobe Dreamweaver' }, { id: 4, name: 'Adobe InDesign' }, { id: 5, name: 'Adobe Connect' } ] } ); let { dragAndDropHooks } = useDragAndDrop({ getItems: (keys) => [...keys].map( (key) => ({ 'text/plain': list.getItem( key ).name }) ), onReorder(e) { if ( e.target .dropPosition === 'before' ) { list.moveBefore( e.target.key, e.keys ); } else if ( e.target .dropPosition === 'after' ) { list.moveAfter( e.target.key, e.keys ); } } }); return ( <MyGridList aria-label="Reorderable list" selectionMode="multiple" items={list.items} dragAndDropHooks={dragAndDropHooks} > {(item) => ( <MyItem> {item.name} </MyItem> )} </MyGridList> ); } ≡ Adobe Photoshop ≡ Adobe XD ≡ Adobe Dreamweaver ≡ Adobe InDesign ≡ Adobe Connect Show CSS .react-aria-GridListItem { &[data-allows-dragging] { padding-left: 4px; } &[data-dragging] { opacity: 0.6; } [slot=drag] { all: unset; width: 15px; text-align: center; &[data-focus-visible] { border-radius: 4px; outline: 2px solid var(--focus-ring-color); } } } .react-aria-DropIndicator { &[data-drop-target] { outline: 1px solid var(--highlight-background); } @supports not selector(:has(.foo)) { /* Undo gap in browsers that don't support :has */ margin-bottom: -2px; } } .react-aria-GridListItem { &[data-allows-dragging] { padding-left: 4px; } &[data-dragging] { opacity: 0.6; } [slot=drag] { all: unset; width: 15px; text-align: center; &[data-focus-visible] { border-radius: 4px; outline: 2px solid var(--focus-ring-color); } } } .react-aria-DropIndicator { &[data-drop-target] { outline: 1px solid var(--highlight-background); } @supports not selector(:has(.foo)) { /* Undo gap in browsers that don't support :has */ margin-bottom: -2px; } } .react-aria-GridListItem { &[data-allows-dragging] { padding-left: 4px; } &[data-dragging] { opacity: 0.6; } [slot=drag] { all: unset; width: 15px; text-align: center; &[data-focus-visible] { border-radius: 4px; outline: 2px solid var(--focus-ring-color); } } } .react-aria-DropIndicator { &[data-drop-target] { outline: 1px solid var(--highlight-background); } @supports not selector(:has(.foo)) { /* Undo gap in browsers that don't support :has */ margin-bottom: -2px; } } ### Custom drag preview# By default, the drag preview shown under the user's pointer or finger is a copy of the original element that started the drag. A custom preview can be rendered by implementing the `renderDragPreview` function, passed to `useDragAndDrop`. This receives the dragged data that was returned by `getItems`, and returns a rendered preview for those items. This example renders a custom drag preview which shows the number of items being dragged. import {useListData} from 'react-stately'; import {useDragAndDrop} from 'react-aria-components'; function Example() { let {dragAndDropHooks} = useDragAndDrop({ // ... renderDragPreview(items) { return ( <div className="drag-preview"> {items[0]['text/plain']} <span className="badge">{items.length}</span> </div> ); } }); // ... } import {useListData} from 'react-stately'; import {useDragAndDrop} from 'react-aria-components'; function Example() { let {dragAndDropHooks} = useDragAndDrop({ // ... renderDragPreview(items) { return ( <div className="drag-preview"> {items[0]['text/plain']} <span className="badge">{items.length}</span> </div> ); } }); // ... } import {useListData} from 'react-stately'; import {useDragAndDrop} from 'react-aria-components'; function Example() { let { dragAndDropHooks } = useDragAndDrop({ // ... renderDragPreview( items ) { return ( <div className="drag-preview"> {items[0][ 'text/plain' ]} <span className="badge"> {items .length} </span> </div> ); } }); // ... } ≡ Adobe Photoshop ≡ Adobe XD ≡ Adobe Dreamweaver ≡ Adobe InDesign ≡ Adobe Connect Show CSS .drag-preview { width: 150px; padding: 4px 8px; display: flex; align-items: center; justify-content: space-between; gap: 4px; background: var(--highlight-background); color: var(--highlight-foreground); border-radius: 4px; .badge { background: var(--highlight-foreground); color: var(--highlight-background); padding: 0 8px; border-radius: 4px; } } .drag-preview { width: 150px; padding: 4px 8px; display: flex; align-items: center; justify-content: space-between; gap: 4px; background: var(--highlight-background); color: var(--highlight-foreground); border-radius: 4px; .badge { background: var(--highlight-foreground); color: var(--highlight-background); padding: 0 8px; border-radius: 4px; } } .drag-preview { width: 150px; padding: 4px 8px; display: flex; align-items: center; justify-content: space-between; gap: 4px; background: var(--highlight-background); color: var(--highlight-foreground); border-radius: 4px; .badge { background: var(--highlight-foreground); color: var(--highlight-background); padding: 0 8px; border-radius: 4px; } } ### Drag data# Data for draggable items can be provided in multiple formats at once. This allows drop targets to choose data in a format that they understand. For example, you could serialize a complex object as JSON in a custom format for use within your own application, and also provide plain text and/or rich HTML fallbacks that can be used when a user drops data in an external application (e.g. an email message). This can be done by returning multiple keys for an item from the `getItems` function. Types can either be a standard mime type for interoperability with external applications, or a custom string for use within your own app. This example provides representations of each item as plain text, HTML, and a custom app-specific data format. Dropping on the drop targets in this page will use the custom data format to render formatted items. If you drop in an external application supporting rich text, the HTML representation will be used. Dropping in a text editor will use the plain text format. function DraggableGridList() { let items = new Map([ ['ps', { name: 'Photoshop', style: 'strong' }], ['xd', { name: 'XD', style: 'strong' }], ['id', { name: 'InDesign', style: 'strong' }], ['dw', { name: 'Dreamweaver', style: 'em' }], ['co', { name: 'Connect', style: 'em' }] ]); let { dragAndDropHooks } = useDragAndDrop({ getItems(keys) { return [...keys].map((key) => { let item = items.get(key as string)!; return { 'text/plain': item.name, 'text/html': `<${item.style}>${item.name}</${item.style}>`, 'custom-app-type': JSON.stringify({ id: key, ...item }) }; }); } }); return ( <MyGridList aria-label="Draggable list" selectionMode="multiple" items={items} dragAndDropHooks={dragAndDropHooks} > {([id, item]) => ( <MyItem id={id} textValue={item.name}> {React.createElement(item.style || 'span', null, item.name)} </MyItem> )} </MyGridList> ); } <div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }}> <DraggableGridList /> {/* see below */} <DroppableGridList /> </div> function DraggableGridList() { let items = new Map([ ['ps', { name: 'Photoshop', style: 'strong' }], ['xd', { name: 'XD', style: 'strong' }], ['id', { name: 'InDesign', style: 'strong' }], ['dw', { name: 'Dreamweaver', style: 'em' }], ['co', { name: 'Connect', style: 'em' }] ]); let { dragAndDropHooks } = useDragAndDrop({ getItems(keys) { return [...keys].map((key) => { let item = items.get(key as string)!; return { 'text/plain': item.name, 'text/html': `<${item.style}>${item.name}</${item.style}>`, 'custom-app-type': JSON.stringify({ id: key, ...item }) }; }); } }); return ( <MyGridList aria-label="Draggable list" selectionMode="multiple" items={items} dragAndDropHooks={dragAndDropHooks} > {([id, item]) => ( <MyItem id={id} textValue={item.name}> {React.createElement( item.style || 'span', null, item.name )} </MyItem> )} </MyGridList> ); } <div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }} > <DraggableGridList /> {/* see below */} <DroppableGridList /> </div> function DraggableGridList() { let items = new Map([ ['ps', { name: 'Photoshop', style: 'strong' }], ['xd', { name: 'XD', style: 'strong' }], ['id', { name: 'InDesign', style: 'strong' }], ['dw', { name: 'Dreamweaver', style: 'em' }], ['co', { name: 'Connect', style: 'em' }] ]); let { dragAndDropHooks } = useDragAndDrop({ getItems(keys) { return [...keys] .map((key) => { let item = items.get( key as string )!; return { 'text/plain': item.name, 'text/html': `<${item.style}>${item.name}</${item.style}>`, 'custom-app-type': JSON .stringify( { id: key, ...item } ) }; }); } }); return ( <MyGridList aria-label="Draggable list" selectionMode="multiple" items={items} dragAndDropHooks={dragAndDropHooks} > {([id, item]) => ( <MyItem id={id} textValue={item .name} > {React .createElement( item .style || 'span', null, item.name )} </MyItem> )} </MyGridList> ); } <div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }} > <DraggableGridList /> {/* see below */} <DroppableGridList /> </div> ≡ **Photoshop** ≡ **XD** ≡ **InDesign** ≡ _Dreamweaver_ ≡ _Connect_ Drop items here ### Dropping on the collection# Dropping on the GridList as a whole can be enabled using the `onRootDrop` event. When a valid drag hovers over the GridList, it receives the `isDropTarget` state and can be styled using the `[data-drop-target]` CSS selector. interface Item { id: number; name: string; } function Example() { let [items, setItems] = React.useState<Item[]>([]); let { dragAndDropHooks } = useDragAndDrop({ async onRootDrop(e) { let items = await Promise.all(e.items.map(async (item, i) => { let name = item.kind === 'text' ? await item.getText('text/plain') : item.name; return { id: i, name }; })); setItems(items); } }); return ( <div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }}> <DraggableGridList /> <MyGridList aria-label="Droppable list" items={items} dragAndDropHooks={dragAndDropHooks} renderEmptyState={() => 'Drop items here'} > {(item) => <GridListItem>{item.name}</GridListItem>} </MyGridList> </div> ); } interface Item { id: number; name: string; } function Example() { let [items, setItems] = React.useState<Item[]>([]); let { dragAndDropHooks } = useDragAndDrop({ async onRootDrop(e) { let items = await Promise.all( e.items.map(async (item, i) => { let name = item.kind === 'text' ? await item.getText('text/plain') : item.name; return { id: i, name }; }) ); setItems(items); } }); return ( <div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }} > <DraggableGridList /> <MyGridList aria-label="Droppable list" items={items} dragAndDropHooks={dragAndDropHooks} renderEmptyState={() => 'Drop items here'} > {(item) => <GridListItem>{item.name}</GridListItem>} </MyGridList> </div> ); } interface Item { id: number; name: string; } function Example() { let [items, setItems] = React.useState< Item[] >([]); let { dragAndDropHooks } = useDragAndDrop({ async onRootDrop(e) { let items = await Promise .all( e.items.map( async ( item, i ) => { let name = item .kind === 'text' ? await item .getText( 'text/plain' ) : item .name; return { id: i, name }; } ) ); setItems(items); } }); return ( <div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }} > <DraggableGridList /> <MyGridList aria-label="Droppable list" items={items} dragAndDropHooks={dragAndDropHooks} renderEmptyState={() => 'Drop items here'} > {(item) => ( <GridListItem> {item.name} </GridListItem> )} </MyGridList> </div> ); } ≡ **Photoshop** ≡ **XD** ≡ **InDesign** ≡ _Dreamweaver_ ≡ _Connect_ Drop items here Show CSS .react-aria-GridList[data-drop-target] { outline: 2px solid var(--highlight-background); outline-offset: -1px; background: var(--highlight-overlay); } .react-aria-GridList[data-drop-target] { outline: 2px solid var(--highlight-background); outline-offset: -1px; background: var(--highlight-overlay); } .react-aria-GridList[data-drop-target] { outline: 2px solid var(--highlight-background); outline-offset: -1px; background: var(--highlight-overlay); } ### Dropping on items# Dropping on items can be enabled using the `onItemDrop` event. When a valid drag hovers over an item, it receives the `isDropTarget` state and can be styled using the `[data-drop-target]` CSS selector. function Example() { let { dragAndDropHooks } = useDragAndDrop({ onItemDrop(e) { alert(`Dropped on ${e.target.key}`); } }); return ( <div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }}> {/* see above */} <DraggableGridList /> <MyGridList aria-label="Droppable list" dragAndDropHooks={dragAndDropHooks} > <MyItem id="applications">Applications</MyItem> <MyItem id="documents">Documents</MyItem> <MyItem id="pictures">Pictures</MyItem> </MyGridList> </div> ); } function Example() { let { dragAndDropHooks } = useDragAndDrop({ onItemDrop(e) { alert(`Dropped on ${e.target.key}`); } }); return ( <div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }} > {/* see above */} <DraggableGridList /> <MyGridList aria-label="Droppable list" dragAndDropHooks={dragAndDropHooks} > <MyItem id="applications">Applications</MyItem> <MyItem id="documents">Documents</MyItem> <MyItem id="pictures">Pictures</MyItem> </MyGridList> </div> ); } function Example() { let { dragAndDropHooks } = useDragAndDrop({ onItemDrop(e) { alert( `Dropped on ${e.target.key}` ); } }); return ( <div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }} > {/* see above */} <DraggableGridList /> <MyGridList aria-label="Droppable list" dragAndDropHooks={dragAndDropHooks} > <MyItem id="applications"> Applications </MyItem> <MyItem id="documents"> Documents </MyItem> <MyItem id="pictures"> Pictures </MyItem> </MyGridList> </div> ); } ≡ **Photoshop** ≡ **XD** ≡ **InDesign** ≡ _Dreamweaver_ ≡ _Connect_ Applications Documents Pictures Show CSS .react-aria-GridListItem[data-drop-target] { outline: 2px solid var(--highlight-background); background:var(--highlight-overlay); } .react-aria-GridListItem[data-drop-target] { outline: 2px solid var(--highlight-background); background:var(--highlight-overlay); } .react-aria-GridListItem[data-drop-target] { outline: 2px solid var(--highlight-background); background:var(--highlight-overlay); } ### Dropping between items# Dropping between items can be enabled using the `onInsert` event. GridList renders a `DropIndicator` between items to indicate the insertion position, which can be styled using the `.react-aria-DropIndicator` selector. When it is active, it receives the `[data-drop-target]` state. function Example() { let list = useListData({ initialItems: [ { id: 1, name: 'Illustrator' }, { id: 2, name: 'Premiere' }, { id: 3, name: 'Acrobat' } ] }); let { dragAndDropHooks } = useDragAndDrop({ async onInsert(e) { let items = await Promise.all(e.items.map(async (item) => { let name = item.kind === 'text' ? await item.getText('text/plain') : item.name; return { id: Math.random(), name }; })); if (e.target.dropPosition === 'before') { list.insertBefore(e.target.key, ...items); } else if (e.target.dropPosition === 'after') { list.insertAfter(e.target.key, ...items); } } }); return ( <div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }}> <DraggableGridList /> <MyGridList aria-label="Droppable list" items={list.items} dragAndDropHooks={dragAndDropHooks} > {(item) => <GridListItem>{item.name}</GridListItem>} </MyGridList> </div> ); } function Example() { let list = useListData({ initialItems: [ { id: 1, name: 'Illustrator' }, { id: 2, name: 'Premiere' }, { id: 3, name: 'Acrobat' } ] }); let { dragAndDropHooks } = useDragAndDrop({ async onInsert(e) { let items = await Promise.all( e.items.map(async (item) => { let name = item.kind === 'text' ? await item.getText('text/plain') : item.name; return { id: Math.random(), name }; }) ); if (e.target.dropPosition === 'before') { list.insertBefore(e.target.key, ...items); } else if (e.target.dropPosition === 'after') { list.insertAfter(e.target.key, ...items); } } }); return ( <div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }} > <DraggableGridList /> <MyGridList aria-label="Droppable list" items={list.items} dragAndDropHooks={dragAndDropHooks} > {(item) => <GridListItem>{item.name}</GridListItem>} </MyGridList> </div> ); } function Example() { let list = useListData( { initialItems: [ { id: 1, name: 'Illustrator' }, { id: 2, name: 'Premiere' }, { id: 3, name: 'Acrobat' } ] } ); let { dragAndDropHooks } = useDragAndDrop({ async onInsert(e) { let items = await Promise .all( e.items.map( async (item) => { let name = item .kind === 'text' ? await item .getText( 'text/plain' ) : item .name; return { id: Math .random(), name }; } ) ); if ( e.target .dropPosition === 'before' ) { list .insertBefore( e.target.key, ...items ); } else if ( e.target .dropPosition === 'after' ) { list.insertAfter( e.target.key, ...items ); } } }); return ( <div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }} > <DraggableGridList /> <MyGridList aria-label="Droppable list" items={list .items} dragAndDropHooks={dragAndDropHooks} > {(item) => ( <GridListItem> {item.name} </GridListItem> )} </MyGridList> </div> ); } ≡ **Photoshop** ≡ **XD** ≡ **InDesign** ≡ _Dreamweaver_ ≡ _Connect_ Illustrator Premiere Acrobat Show CSS .react-aria-DropIndicator { &[data-drop-target] { outline: 1px solid var(--highlight-background); } @supports not selector(:has(.foo)) { /* Undo gap in browsers that don't support :has */ margin-bottom: -2px; } } .react-aria-DropIndicator { &[data-drop-target] { outline: 1px solid var(--highlight-background); } @supports not selector(:has(.foo)) { /* Undo gap in browsers that don't support :has */ margin-bottom: -2px; } } .react-aria-DropIndicator { &[data-drop-target] { outline: 1px solid var(--highlight-background); } @supports not selector(:has(.foo)) { /* Undo gap in browsers that don't support :has */ margin-bottom: -2px; } } A custom drop indicator can also be rendered with the `renderDropIndicator` function. This lets you customize the DOM structure and CSS classes applied to the drop indicator. import {DropIndicator} from 'react-aria-components'; function Example() { let { dragAndDropHooks } = useDragAndDrop({ // ... renderDropIndicator(target) { return ( <DropIndicator target={target} className={({ isDropTarget }) => `my-drop-indicator ${isDropTarget ? 'active' : ''}`} /> ); } }); // ... } import {DropIndicator} from 'react-aria-components'; function Example() { let { dragAndDropHooks } = useDragAndDrop({ // ... renderDropIndicator(target) { return ( <DropIndicator target={target} className={({ isDropTarget }) => `my-drop-indicator ${ isDropTarget ? 'active' : '' }`} /> ); } }); // ... } import {DropIndicator} from 'react-aria-components'; function Example() { let { dragAndDropHooks } = useDragAndDrop({ // ... renderDropIndicator( target ) { return ( <DropIndicator target={target} className={( { isDropTarget } ) => `my-drop-indicator ${ isDropTarget ? 'active' : '' }`} /> ); } }); // ... } ≡ **Photoshop** ≡ **XD** ≡ **InDesign** ≡ _Dreamweaver_ ≡ _Connect_ Illustrator Premiere Acrobat Show CSS .my-drop-indicator { &.active { outline: 1px solid #e70073; } @supports not selector(:has(.foo)) { /* Undo gap in browsers that don't support :has */ margin-bottom: -2px; } } .my-drop-indicator { &.active { outline: 1px solid #e70073; } @supports not selector(:has(.foo)) { /* Undo gap in browsers that don't support :has */ margin-bottom: -2px; } } .my-drop-indicator { &.active { outline: 1px solid #e70073; } @supports not selector(:has(.foo)) { /* Undo gap in browsers that don't support :has */ margin-bottom: -2px; } } ### Drop data# `GridList` allows users to drop one or more **drag items**, each of which contains data to be transferred from the drag source to drop target. There are three kinds of drag items: * `text` – represents data inline as a string in one or more formats * `file` – references a file on the user's device * `directory` – references the contents of a directory #### Text# A `TextDropItem` represents textual data in one or more different formats. These may be either standard mime types or custom app-specific formats. Representing data in multiple formats allows drop targets both within and outside an application to choose data in a format that they understand. For example, a complex object may be serialized in a custom format for use within an application, with fallbacks in plain text and/or rich HTML that can be used when a user drops data from an external application. The example below uses the `acceptedDragTypes` prop to accept items that include a custom app-specific type, which is retrieved using the item's `getText` method. The same draggable component as used in the above example is used here, but rather than displaying the plain text representation, the custom format is used instead. When `acceptedDragTypes` is specified, the dropped items are filtered to include only items that include the accepted types. import {isTextDropItem} from 'react-aria-components'; interface TextItem { id: string; name: string; style: string; } function DroppableGridList() { let [items, setItems] = React.useState<TextItem[]>([]); let { dragAndDropHooks } = useDragAndDrop({ acceptedDragTypes: ['custom-app-type'], async onRootDrop(e) { let items = await Promise.all( e.items .filter(isTextDropItem) .map(async (item) => JSON.parse(await item.getText('custom-app-type')) ) ); setItems(items); } }); return ( <MyGridList aria-label="Droppable list" items={items} dragAndDropHooks={dragAndDropHooks} renderEmptyState={() => 'Drop items here'} > {(item) => ( <MyItem textValue={item.name}> {React.createElement(item.style || 'span', null, item.name)} </MyItem> )} </MyGridList> ); } <div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }}> {/* see above */} <DraggableGridList /> <DroppableGridList /> </div> import {isTextDropItem} from 'react-aria-components'; interface TextItem { id: string; name: string; style: string; } function DroppableGridList() { let [items, setItems] = React.useState<TextItem[]>([]); let { dragAndDropHooks } = useDragAndDrop({ acceptedDragTypes: ['custom-app-type'], async onRootDrop(e) { let items = await Promise.all( e.items .filter(isTextDropItem) .map(async (item) => JSON.parse( await item.getText('custom-app-type') ) ) ); setItems(items); } }); return ( <MyGridList aria-label="Droppable list" items={items} dragAndDropHooks={dragAndDropHooks} renderEmptyState={() => 'Drop items here'} > {(item) => ( <MyItem textValue={item.name}> {React.createElement( item.style || 'span', null, item.name )} </MyItem> )} </MyGridList> ); } <div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }} > {/* see above */} <DraggableGridList /> <DroppableGridList /> </div> import {isTextDropItem} from 'react-aria-components'; interface TextItem { id: string; name: string; style: string; } function DroppableGridList() { let [items, setItems] = React.useState< TextItem[] >([]); let { dragAndDropHooks } = useDragAndDrop({ acceptedDragTypes: [ 'custom-app-type' ], async onRootDrop(e) { let items = await Promise .all( e.items .filter( isTextDropItem ) .map( async (item) => JSON .parse( await item .getText( 'custom-app-type' ) ) ) ); setItems(items); } }); return ( <MyGridList aria-label="Droppable list" items={items} dragAndDropHooks={dragAndDropHooks} renderEmptyState={() => 'Drop items here'} > {(item) => ( <MyItem textValue={item .name} > {React .createElement( item .style || 'span', null, item.name )} </MyItem> )} </MyGridList> ); } <div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }} > {/* see above */} <DraggableGridList /> <DroppableGridList /> </div> ≡ **Photoshop** ≡ **XD** ≡ **InDesign** ≡ _Dreamweaver_ ≡ _Connect_ Drop items here #### Files# A `FileDropItem` references a file on the user's device. It includes the name and mime type of the file, and methods to read the contents as plain text, or retrieve a native File object which can be attached to form data for uploading. This example accepts JPEG and PNG image files, and renders them by creating a local object URL. When the list is empty, you can drop on the whole collection, and otherwise items can be inserted. import {isFileDropItem} from 'react-aria-components'; interface ImageItem { id: number; url: string; name: string; } function Example() { let [items, setItems] = React.useState<ImageItem[]>([]); let { dragAndDropHooks } = useDragAndDrop({ acceptedDragTypes: ['image/jpeg', 'image/png'], async onRootDrop(e) { let items = await Promise.all( e.items.filter(isFileDropItem).map(async (item) => ({ id: Math.random(), url: URL.createObjectURL(await item.getFile()), name: item.name })) ); setItems(items); } }); return ( <MyGridList aria-label="Droppable list" items={items} dragAndDropHooks={dragAndDropHooks} renderEmptyState={() => 'Drop images here'} > {(item) => ( <MyItem textValue={item.name}> <div className="image-item"> <img src={item.url} /> <span>{item.name}</span> </div> </MyItem> )} </MyGridList> ); } import {isFileDropItem} from 'react-aria-components'; interface ImageItem { id: number; url: string; name: string; } function Example() { let [items, setItems] = React.useState<ImageItem[]>([]); let { dragAndDropHooks } = useDragAndDrop({ acceptedDragTypes: ['image/jpeg', 'image/png'], async onRootDrop(e) { let items = await Promise.all( e.items.filter(isFileDropItem).map( async (item) => ({ id: Math.random(), url: URL.createObjectURL(await item.getFile()), name: item.name }) ) ); setItems(items); } }); return ( <MyGridList aria-label="Droppable list" items={items} dragAndDropHooks={dragAndDropHooks} renderEmptyState={() => 'Drop images here'} > {(item) => ( <MyItem textValue={item.name}> <div className="image-item"> <img src={item.url} /> <span>{item.name}</span> </div> </MyItem> )} </MyGridList> ); } import {isFileDropItem} from 'react-aria-components'; interface ImageItem { id: number; url: string; name: string; } function Example() { let [items, setItems] = React.useState< ImageItem[] >([]); let { dragAndDropHooks } = useDragAndDrop({ acceptedDragTypes: [ 'image/jpeg', 'image/png' ], async onRootDrop(e) { let items = await Promise .all( e.items .filter( isFileDropItem ).map( async (item) => ({ id: Math .random(), url: URL .createObjectURL( await item .getFile() ), name: item .name }) ) ); setItems(items); } }); return ( <MyGridList aria-label="Droppable list" items={items} dragAndDropHooks={dragAndDropHooks} renderEmptyState={() => 'Drop images here'} > {(item) => ( <MyItem textValue={item .name} > <div className="image-item"> <img src={item .url} /> <span> {item.name} </span> </div> </MyItem> )} </MyGridList> ); } Drop images here Show CSS .image-item { display: flex; height: 50px; gap: 10px; align-items: center; } .image-item img { height: 100%; aspect-ratio: 1/1; object-fit: contain; } .image-item span { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .image-item { display: flex; height: 50px; gap: 10px; align-items: center; } .image-item img { height: 100%; aspect-ratio: 1/1; object-fit: contain; } .image-item span { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .image-item { display: flex; height: 50px; gap: 10px; align-items: center; } .image-item img { height: 100%; aspect-ratio: 1/1; object-fit: contain; } .image-item span { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } #### Directories# A `DirectoryDropItem` references the contents of a directory on the user's device. It includes the name of the directory, as well as a method to iterate through the files and folders within the directory. The contents of any folders within the directory can be accessed recursively. The `getEntries` method returns an async iterable object, which can be used in a `for await...of` loop. This provides each item in the directory as either a `FileDropItem` or` DirectoryDropItem `, and you can access the contents of each file as discussed above. This example accepts directory drops over the whole collection, and renders the contents as items in the list. `DIRECTORY_DRAG_TYPE` is imported from `react-aria-components` and included in the `acceptedDragTypes` prop to limit the accepted items to only directories. import File from '@spectrum-icons/workflow/FileTxt'; import Folder from '@spectrum-icons/workflow/Folder'; import {DIRECTORY_DRAG_TYPE, isDirectoryDropItem} from 'react-aria-components'; interface DirItem { name: string; kind: string; } function Example() { let [files, setFiles] = React.useState<DirItem[]>([]); let { dragAndDropHooks } = useDragAndDrop({ acceptedDragTypes: [DIRECTORY_DRAG_TYPE], async onRootDrop(e) { // Read entries in directory and update state with relevant info. let dir = e.items.find(isDirectoryDropItem)!; let files = []; for await (let entry of dir.getEntries()) { files.push({ name: entry.name, kind: entry.kind }); } setFiles(files); } }); return ( <MyGridList aria-label="Droppable list" items={files} dragAndDropHooks={dragAndDropHooks} renderEmptyState={() => 'Drop directory here'} > {(item) => ( <MyItem id={item.name} textValue={item.name}> <div className="dir-item"> {item.kind === 'directory' ? <Folder /> : <File />} <span>{item.name}</span> </div> </MyItem> )} </MyGridList> ); } import File from '@spectrum-icons/workflow/FileTxt'; import Folder from '@spectrum-icons/workflow/Folder'; import { DIRECTORY_DRAG_TYPE, isDirectoryDropItem } from 'react-aria-components'; interface DirItem { name: string; kind: string; } function Example() { let [files, setFiles] = React.useState<DirItem[]>([]); let { dragAndDropHooks } = useDragAndDrop({ acceptedDragTypes: [DIRECTORY_DRAG_TYPE], async onRootDrop(e) { // Read entries in directory and update state with relevant info. let dir = e.items.find(isDirectoryDropItem)!; let files = []; for await (let entry of dir.getEntries()) { files.push({ name: entry.name, kind: entry.kind }); } setFiles(files); } }); return ( <MyGridList aria-label="Droppable list" items={files} dragAndDropHooks={dragAndDropHooks} renderEmptyState={() => 'Drop directory here'} > {(item) => ( <MyItem id={item.name} textValue={item.name}> <div className="dir-item"> {item.kind === 'directory' ? <Folder /> : <File />} <span>{item.name}</span> </div> </MyItem> )} </MyGridList> ); } import File from '@spectrum-icons/workflow/FileTxt'; import Folder from '@spectrum-icons/workflow/Folder'; import { DIRECTORY_DRAG_TYPE, isDirectoryDropItem } from 'react-aria-components'; interface DirItem { name: string; kind: string; } function Example() { let [files, setFiles] = React.useState< DirItem[] >([]); let { dragAndDropHooks } = useDragAndDrop({ acceptedDragTypes: [ DIRECTORY_DRAG_TYPE ], async onRootDrop(e) { // Read entries in directory and update state with relevant info. let dir = e.items .find( isDirectoryDropItem )!; let files = []; for await ( let entry of dir .getEntries() ) { files.push({ name: entry.name, kind: entry.kind }); } setFiles(files); } }); return ( <MyGridList aria-label="Droppable list" items={files} dragAndDropHooks={dragAndDropHooks} renderEmptyState={() => 'Drop directory here'} > {(item) => ( <MyItem id={item.name} textValue={item .name} > <div className="dir-item"> {item .kind === 'directory' ? ( <Folder /> ) : <File />} <span> {item.name} </span> </div> </MyItem> )} </MyGridList> ); } Drop directory here Show CSS .dir-item { display: flex; align-items: center; gap: 8px; } .dir-item { flex: 0 0 auto; } .dir-item { flex: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .dir-item { display: flex; align-items: center; gap: 8px; } .dir-item { flex: 0 0 auto; } .dir-item { flex: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .dir-item { display: flex; align-items: center; gap: 8px; } .dir-item { flex: 0 0 auto; } .dir-item { flex: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } ### Drop operations# A `DropOperation` is an indication of what will happen when dragged data is dropped on a particular drop target. These are: * `move` – indicates that the dragged data will be moved from its source location to the target location. * `copy` – indicates that the dragged data will be copied to the target destination. * `link` – indicates that there will be a relationship established between the source and target locations. * `cancel` – indicates that the drag and drop operation will be canceled, resulting in no changes made to the source or target. Many operating systems display these in the form of a cursor change, e.g. a plus sign to indicate a copy operation. The user may also be able to use a modifier key to choose which drop operation to perform, such as Option or Alt to switch from move to copy. #### onDragEnd# The `onDragEnd` event allows the drag source to respond when a drag that it initiated ends, either because it was dropped or because it was canceled by the user. The `dropOperation` property of the event object indicates the operation that was performed. For example, when data is moved, the UI could be updated to reflect this change by removing the original dragged items. This example removes the dragged items from the UI when a move operation is completed. Try holding the Option or Alt keys to change the operation to copy, and see how the behavior changes. function Example() { let list = useListData({ initialItems: [ { id: 1, name: 'Adobe Photoshop' }, { id: 2, name: 'Adobe XD' }, { id: 3, name: 'Adobe Dreamweaver' }, { id: 4, name: 'Adobe InDesign' }, { id: 5, name: 'Adobe Connect' } ] }); let { dragAndDropHooks } = useDragAndDrop({ // ... onDragEnd(e) { if (e.dropOperation === 'move') { list.remove(...e.keys); } } }); return ( <div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }}> <MyGridList aria-label="Draggable list" selectionMode="multiple" items={list.items} dragAndDropHooks={dragAndDropHooks} > {(item) => <MyItem>{item.name}</MyItem>} </MyGridList> <DroppableGridList /> </div> ); } function Example() { let list = useListData({ initialItems: [ { id: 1, name: 'Adobe Photoshop' }, { id: 2, name: 'Adobe XD' }, { id: 3, name: 'Adobe Dreamweaver' }, { id: 4, name: 'Adobe InDesign' }, { id: 5, name: 'Adobe Connect' } ] }); let { dragAndDropHooks } = useDragAndDrop({ // ... onDragEnd(e) { if (e.dropOperation === 'move') { list.remove(...e.keys); } } }); return ( <div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }} > <MyGridList aria-label="Draggable list" selectionMode="multiple" items={list.items} dragAndDropHooks={dragAndDropHooks} > {(item) => <MyItem>{item.name}</MyItem>} </MyGridList> <DroppableGridList /> </div> ); } function Example() { let list = useListData( { initialItems: [ { id: 1, name: 'Adobe Photoshop' }, { id: 2, name: 'Adobe XD' }, { id: 3, name: 'Adobe Dreamweaver' }, { id: 4, name: 'Adobe InDesign' }, { id: 5, name: 'Adobe Connect' } ] } ); let { dragAndDropHooks } = useDragAndDrop({ // ... onDragEnd(e) { if ( e.dropOperation === 'move' ) { list.remove( ...e.keys ); } } }); return ( <div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }} > <MyGridList aria-label="Draggable list" selectionMode="multiple" items={list .items} dragAndDropHooks={dragAndDropHooks} > {(item) => ( <MyItem> {item.name} </MyItem> )} </MyGridList> <DroppableGridList /> </div> ); } ≡ Adobe Photoshop ≡ Adobe XD ≡ Adobe Dreamweaver ≡ Adobe InDesign ≡ Adobe Connect Drop items here #### getAllowedDropOperations# The drag source can also control which drop operations are allowed for the data. For example, if moving data is not allowed, and only copying is supported, the `getAllowedDropOperations` function could be implemented to indicate this. When you drag the element below, the cursor now shows the copy affordance by default, and pressing a modifier to switch drop operations results in the drop being canceled. function Example() { // ... let { dragAndDropHooks } = useDragAndDrop({ // ... getAllowedDropOperations: () => ['copy'] }); return ( <div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }}> <MyGridList aria-label="Draggable list" selectionMode="multiple" items={list.items} dragAndDropHooks={dragAndDropHooks} > {(item) => <MyItem>{item.name}</MyItem>} </MyGridList> <DroppableGridList /> </div> ); } function Example() { // ... let { dragAndDropHooks } = useDragAndDrop({ // ... getAllowedDropOperations: () => ['copy'] }); return ( <div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }} > <MyGridList aria-label="Draggable list" selectionMode="multiple" items={list.items} dragAndDropHooks={dragAndDropHooks} > {(item) => <MyItem>{item.name}</MyItem>} </MyGridList> <DroppableGridList /> </div> ); } function Example() { // ... let { dragAndDropHooks } = useDragAndDrop({ // ... getAllowedDropOperations: () => ['copy'] }); return ( <div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }} > <MyGridList aria-label="Draggable list" selectionMode="multiple" items={list .items} dragAndDropHooks={dragAndDropHooks} > {(item) => ( <MyItem> {item.name} </MyItem> )} </MyGridList> <DroppableGridList /> </div> ); } ≡ Adobe Photoshop ≡ Adobe XD ≡ Adobe Dreamweaver ≡ Adobe InDesign ≡ Adobe Connect Drop items here #### getDropOperation# The `getDropOperation` function passed to `useDragAndDrop` can be used to provide appropriate feedback to the user when a drag hovers over the drop target. This function receives the drop target, set of types contained in the drag, and a list of allowed drop operations as specified by the drag source. It should return one of the drop operations in `allowedOperations`, or a specific drop operation if only that drop operation is supported. It may also return `'cancel'` to reject the drop. If the returned operation is not in `allowedOperations`, then the drop target will act as if `'cancel'` was returned. In the below example, the drop target only supports dropping PNG images. If a PNG is dragged over the target, it will be highlighted and the operating system displays a copy cursor. If another type is dragged over the target, then there is no visual feedback, indicating that a drop is not accepted there. If the user holds a modifier key such as Control while dragging over the drop target in order to change the drop operation, then the drop target does not accept the drop. function Example() { let [items, setItems] = React.useState<ImageItem[]>([]); let { dragAndDropHooks } = useDragAndDrop({ getDropOperation: () => 'copy', acceptedDragTypes: ['image/png'], async onRootDrop(e) { // ... } }); // See "Files" example above... } function Example() { let [items, setItems] = React.useState<ImageItem[]>([]); let { dragAndDropHooks } = useDragAndDrop({ getDropOperation: () => 'copy', acceptedDragTypes: ['image/png'], async onRootDrop(e) { // ... } }); // See "Files" example above... } function Example() { let [items, setItems] = React.useState< ImageItem[] >([]); let { dragAndDropHooks } = useDragAndDrop({ getDropOperation: () => 'copy', acceptedDragTypes: [ 'image/png' ], async onRootDrop(e) { // ... } }); // See "Files" example above... } Drop PNGs here #### Drop events# Drop events such as `onInsert`, `onItemDrop`, etc. also include the `dropOperation`. This can be used to perform different actions accordingly, for example, when communicating with a backend API. let onItemDrop = async (e) => { let data = JSON.parse(await e.items[0].getText('my-app-file')); switch (e.dropOperation) { case 'move': MyAppFileService.move(data.filePath, props.filePath); break; case 'copy': MyAppFileService.copy(data.filePath, props.filePath); break; case 'link': MyAppFileService.link(data.filePath, props.filePath); break; }}; let onItemDrop = async (e) => { let data = JSON.parse( await e.items[0].getText('my-app-file') ); switch (e.dropOperation) { case 'move': MyAppFileService.move(data.filePath, props.filePath); break; case 'copy': MyAppFileService.copy(data.filePath, props.filePath); break; case 'link': MyAppFileService.link(data.filePath, props.filePath); break; }}; let onItemDrop = async ( e ) => { let data = JSON.parse( await e.items[0] .getText( 'my-app-file' ) ); switch ( e.dropOperation ) { case 'move': MyAppFileService .move( data.filePath, props.filePath ); break; case 'copy': MyAppFileService .copy( data.filePath, props.filePath ); break; case 'link': MyAppFileService .link( data.filePath, props.filePath ); break; }}; ### Drag between lists# This example puts together many of the concepts described above, allowing users to drag items between lists bidirectionally. It also supports reordering items within the same list. When a list is empty, it accepts drops on the whole collection. `getDropOperation` ensures that items are always moved rather than copied, which avoids duplicate items. import {isTextDropItem} from 'react-aria-components'; interface FileItem { id: string, name: string, type: string } interface DndGridListProps { initialItems: FileItem[], 'aria-label': string } function DndGridList(props: DndGridListProps) { let list = useListData({ initialItems: props.initialItems }); let { dragAndDropHooks } = useDragAndDrop({ // Provide drag data in a custom format as well as plain text. getItems(keys) { return [...keys].map((key) => { let item = list.getItem(key); return { 'custom-app-type': JSON.stringify(item), 'text/plain': item.name }; }); }, // Accept drops with the custom format. acceptedDragTypes: ['custom-app-type'], // Ensure items are always moved rather than copied. getDropOperation: () => 'move', // Handle drops between items from other lists. async onInsert(e) { let processedItems = await Promise.all( e.items .filter(isTextDropItem) .map(async item => JSON.parse(await item.getText('custom-app-type'))) ); if (e.target.dropPosition === 'before') { list.insertBefore(e.target.key, ...processedItems); } else if (e.target.dropPosition === 'after') { list.insertAfter(e.target.key, ...processedItems); } }, // Handle drops on the collection when empty. async onRootDrop(e) { let processedItems = await Promise.all( e.items .filter(isTextDropItem) .map(async item => JSON.parse(await item.getText('custom-app-type'))) ); list.append(...processedItems); }, // Handle reordering items within the same list. onReorder(e) { if (e.target.dropPosition === 'before') { list.moveBefore(e.target.key, e.keys); } else if (e.target.dropPosition === 'after') { list.moveAfter(e.target.key, e.keys); } }, // Remove the items from the source list on drop // if they were moved to a different list. onDragEnd(e) { if (e.dropOperation === 'move' && !e.isInternal) { list.remove(...e.keys); } } }); return ( <MyGridList aria-label={props['aria-label']} selectionMode="multiple" selectedKeys={list.selectedKeys} onSelectionChange={list.setSelectedKeys} items={list.items} dragAndDropHooks={dragAndDropHooks} renderEmptyState={() => 'Drop items here'}> {item => <MyItem>{item.name}</MyItem>} </MyGridList> ); } <div style={{display: 'flex', gap: 12, flexWrap: 'wrap'}}> <DndGridList initialItems={[ { id: '1', type: 'file', name: 'Adobe Photoshop' }, { id: '2', type: 'file', name: 'Adobe XD' }, { id: '3', type: 'folder', name: 'Documents' }, { id: '4', type: 'file', name: 'Adobe InDesign' }, { id: '5', type: 'folder', name: 'Utilities' }, { id: '6', type: 'file', name: 'Adobe AfterEffects' } ]} aria-label="First GridList" /> <DndGridList initialItems={[ { id: '7', type: 'folder', name: 'Pictures' }, { id: '8', type: 'file', name: 'Adobe Fresco' }, { id: '9', type: 'folder', name: 'Apps' }, { id: '10', type: 'file', name: 'Adobe Illustrator' }, { id: '11', type: 'file', name: 'Adobe Lightroom' }, { id: '12', type: 'file', name: 'Adobe Dreamweaver' } ]} aria-label="Second GridList" /> </div> import {isTextDropItem} from 'react-aria-components'; interface FileItem { id: string; name: string; type: string; } interface DndGridListProps { initialItems: FileItem[]; 'aria-label': string; } function DndGridList(props: DndGridListProps) { let list = useListData({ initialItems: props.initialItems }); let { dragAndDropHooks } = useDragAndDrop({ // Provide drag data in a custom format as well as plain text. getItems(keys) { return [...keys].map((key) => { let item = list.getItem(key); return { 'custom-app-type': JSON.stringify(item), 'text/plain': item.name }; }); }, // Accept drops with the custom format. acceptedDragTypes: ['custom-app-type'], // Ensure items are always moved rather than copied. getDropOperation: () => 'move', // Handle drops between items from other lists. async onInsert(e) { let processedItems = await Promise.all( e.items .filter(isTextDropItem) .map(async (item) => JSON.parse( await item.getText('custom-app-type') ) ) ); if (e.target.dropPosition === 'before') { list.insertBefore(e.target.key, ...processedItems); } else if (e.target.dropPosition === 'after') { list.insertAfter(e.target.key, ...processedItems); } }, // Handle drops on the collection when empty. async onRootDrop(e) { let processedItems = await Promise.all( e.items .filter(isTextDropItem) .map(async (item) => JSON.parse( await item.getText('custom-app-type') ) ) ); list.append(...processedItems); }, // Handle reordering items within the same list. onReorder(e) { if (e.target.dropPosition === 'before') { list.moveBefore(e.target.key, e.keys); } else if (e.target.dropPosition === 'after') { list.moveAfter(e.target.key, e.keys); } }, // Remove the items from the source list on drop // if they were moved to a different list. onDragEnd(e) { if (e.dropOperation === 'move' && !e.isInternal) { list.remove(...e.keys); } } }); return ( <MyGridList aria-label={props['aria-label']} selectionMode="multiple" selectedKeys={list.selectedKeys} onSelectionChange={list.setSelectedKeys} items={list.items} dragAndDropHooks={dragAndDropHooks} renderEmptyState={() => 'Drop items here'} > {(item) => <MyItem>{item.name}</MyItem>} </MyGridList> ); } <div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }} > <DndGridList initialItems={[ { id: '1', type: 'file', name: 'Adobe Photoshop' }, { id: '2', type: 'file', name: 'Adobe XD' }, { id: '3', type: 'folder', name: 'Documents' }, { id: '4', type: 'file', name: 'Adobe InDesign' }, { id: '5', type: 'folder', name: 'Utilities' }, { id: '6', type: 'file', name: 'Adobe AfterEffects' } ]} aria-label="First GridList" /> <DndGridList initialItems={[ { id: '7', type: 'folder', name: 'Pictures' }, { id: '8', type: 'file', name: 'Adobe Fresco' }, { id: '9', type: 'folder', name: 'Apps' }, { id: '10', type: 'file', name: 'Adobe Illustrator' }, { id: '11', type: 'file', name: 'Adobe Lightroom' }, { id: '12', type: 'file', name: 'Adobe Dreamweaver' } ]} aria-label="Second GridList" /> </div> import {isTextDropItem} from 'react-aria-components'; interface FileItem { id: string; name: string; type: string; } interface DndGridListProps { initialItems: FileItem[]; 'aria-label': string; } function DndGridList( props: DndGridListProps ) { let list = useListData( { initialItems: props .initialItems } ); let { dragAndDropHooks } = useDragAndDrop({ // Provide drag data in a custom format as well as plain text. getItems(keys) { return [...keys] .map((key) => { let item = list .getItem( key ); return { 'custom-app-type': JSON .stringify( item ), 'text/plain': item.name }; }); }, // Accept drops with the custom format. acceptedDragTypes: [ 'custom-app-type' ], // Ensure items are always moved rather than copied. getDropOperation: () => 'move', // Handle drops between items from other lists. async onInsert(e) { let processedItems = await Promise .all( e.items .filter( isTextDropItem ) .map( async (item) => JSON .parse( await item .getText( 'custom-app-type' ) ) ) ); if ( e.target .dropPosition === 'before' ) { list .insertBefore( e.target.key, ...processedItems ); } else if ( e.target .dropPosition === 'after' ) { list.insertAfter( e.target.key, ...processedItems ); } }, // Handle drops on the collection when empty. async onRootDrop(e) { let processedItems = await Promise .all( e.items .filter( isTextDropItem ) .map( async (item) => JSON .parse( await item .getText( 'custom-app-type' ) ) ) ); list.append( ...processedItems ); }, // Handle reordering items within the same list. onReorder(e) { if ( e.target .dropPosition === 'before' ) { list.moveBefore( e.target.key, e.keys ); } else if ( e.target .dropPosition === 'after' ) { list.moveAfter( e.target.key, e.keys ); } }, // Remove the items from the source list on drop // if they were moved to a different list. onDragEnd(e) { if ( e.dropOperation === 'move' && !e.isInternal ) { list.remove( ...e.keys ); } } }); return ( <MyGridList aria-label={props[ 'aria-label' ]} selectionMode="multiple" selectedKeys={list .selectedKeys} onSelectionChange={list .setSelectedKeys} items={list.items} dragAndDropHooks={dragAndDropHooks} renderEmptyState={() => 'Drop items here'} > {(item) => ( <MyItem> {item.name} </MyItem> )} </MyGridList> ); } <div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }} > <DndGridList initialItems={[ { id: '1', type: 'file', name: 'Adobe Photoshop' }, { id: '2', type: 'file', name: 'Adobe XD' }, { id: '3', type: 'folder', name: 'Documents' }, { id: '4', type: 'file', name: 'Adobe InDesign' }, { id: '5', type: 'folder', name: 'Utilities' }, { id: '6', type: 'file', name: 'Adobe AfterEffects' } ]} aria-label="First GridList" /> <DndGridList initialItems={[ { id: '7', type: 'folder', name: 'Pictures' }, { id: '8', type: 'file', name: 'Adobe Fresco' }, { id: '9', type: 'folder', name: 'Apps' }, { id: '10', type: 'file', name: 'Adobe Illustrator' }, { id: '11', type: 'file', name: 'Adobe Lightroom' }, { id: '12', type: 'file', name: 'Adobe Dreamweaver' } ]} aria-label="Second GridList" /> </div> ≡ Adobe Photoshop ≡ Adobe XD ≡ Documents ≡ Adobe InDesign ≡ Utilities ≡ Adobe AfterEffects ≡ Pictures ≡ Adobe Fresco ≡ Apps ≡ Adobe Illustrator ≡ Adobe Lightroom ≡ Adobe Dreamweaver ## Props# * * * ### GridList# | Name | Type | Default | Description | | --- | --- | --- | --- | | `disallowTypeAhead` | `boolean` | `false` | Whether typeahead navigation is disabled. | | `selectionBehavior` | ` SelectionBehavior ` | — | How multiple selection should behave in the collection. | | `dragAndDropHooks` | ` DragAndDropHooks ` | — | The drag and drop hooks returned by `useDragAndDrop` used to enable drag and drop behavior for the GridList. | | `renderEmptyState` | `( (props: GridListRenderProps )) => ReactNode` | — | Provides content to display when there are no items in the list. | | `layout` | `'stack' | 'grid'` | `'stack'` | Whether the items are arranged in a stack or grid. | | `keyboardNavigationBehavior` | `'arrow' | 'tab'` | `'arrow'` | Whether keyboard navigation to focusable elements within grid list items is via the left/right arrow keys or the tab key. | | `escapeKeyBehavior` | `'clearSelection' | 'none'` | `'clearSelection'` | Whether pressing the escape key should clear selection in the grid list or not. Most experiences should not modify this option as it eliminates a keyboard user's ability to easily clear selection. Only use if the escape key is being handled externally or should not trigger selection clearing contextually. | | `autoFocus` | `boolean | FocusStrategy ` | — | Whether to auto focus the gridlist or an option. | | `disabledBehavior` | ` DisabledBehavior ` | — | Whether `disabledKeys` applies to all interactions, or only selection. | | `shouldSelectOnPressUp` | `boolean` | — | Whether selection should occur on press up instead of press down. | | `items` | `Iterable<T>` | — | Item objects in the collection. | | `disabledKeys` | `Iterable<Key>` | — | The item keys that are disabled. These items cannot be selected, focused, or otherwise interacted with. | | `selectionMode` | ` SelectionMode ` | — | The type of selection that is allowed in the collection. | | `disallowEmptySelection` | `boolean` | — | Whether the collection allows empty selection. | | `selectedKeys` | `'all' | Iterable<Key>` | — | The currently selected keys in the collection (controlled). | | `defaultSelectedKeys` | `'all' | Iterable<Key>` | — | The initial selected keys in the collection (uncontrolled). | | `children` | `ReactNode | ( (item: object )) => ReactNode` | — | The contents of the collection. | | `dependencies` | `ReadonlyArray<any>` | — | Values that should invalidate the item cache when using dynamic collections. | | `className` | `string | ( (values: GridListRenderProps & & { defaultClassName: string | | undefined } )) => string` | — | The CSS className for the element. A function may be provided to compute the class based on component state. | | `style` | `CSSProperties | ( (values: GridListRenderProps & & { defaultStyle: CSSProperties } )) => CSSProperties | undefined` | — | The inline style for the element. A function may be provided to compute the style based on component state. | Events | Name | Type | Description | | --- | --- | --- | | `onAction` | `( (key: Key )) => void` | Handler that is called when a user performs an action on an item. The exact user event depends on the collection's `selectionBehavior` prop and the interaction modality. | | `onSelectionChange` | `( (keys: Selection )) => void` | Handler that is called when the selection changes. | | `onScroll` | `( (e: UIEvent<Element> )) => void` | Handler that is called when a user scrolls. See MDN. | Layout | Name | Type | Description | | --- | --- | --- | | `slot` | `string | null` | A slot name for the component. Slots allow the component to receive props from a parent component. An explicit `null` value indicates that the local props completely override all props received from a parent. | Accessibility | Name | Type | Description | | --- | --- | --- | | `id` | `string` | The element's unique identifier. See MDN. | | `aria-label` | `string` | Defines a string value that labels the current element. | | `aria-labelledby` | `string` | Identifies the element (or elements) that labels the current element. | | `aria-describedby` | `string` | Identifies the element (or elements) that describes the object. | | `aria-details` | `string` | Identifies the element (or elements) that provide a detailed, extended description for the object. | ### GridListItem# A `<GridListItem>` defines a single option within a `<GridList>`. If the `children` are not plain text, then the `textValue` prop must also be set to a plain text representation, which will be used for typeahead in the GridList. Show props | Name | Type | Description | | --- | --- | --- | | `id` | `Key` | The unique id of the item. | | `value` | `object` | The object value that this item represents. When using dynamic collections, this is set automatically. | | `textValue` | `string` | A string representation of the item's contents, used for features like typeahead. | | `isDisabled` | `boolean` | Whether the item is disabled. | | `children` | `ReactNode | ( (values: GridListItemRenderProps & & { defaultChildren: ReactNode | | undefined } )) => ReactNode` | The children of the component. A function may be provided to alter the children based on component state. | | `className` | `string | ( (values: GridListItemRenderProps & & { defaultClassName: string | | undefined } )) => string` | The CSS className for the element. A function may be provided to compute the class based on component state. | | `style` | `CSSProperties | ( (values: GridListItemRenderProps & & { defaultStyle: CSSProperties } )) => CSSProperties | undefined` | The inline style for the element. A function may be provided to compute the style based on component state. | | `href` | ` Href ` | A URL to link to. See MDN. | | `hrefLang` | `string` | Hints at the human language of the linked URL. SeeMDN. | | `target` | `HTMLAttributeAnchorTarget` | The target window for the link. See MDN. | | `rel` | `string` | The relationship between the linked resource and the current page. See MDN. | | `download` | `boolean | string` | Causes the browser to download the linked URL. A string may be provided to suggest a file name. See MDN. | | `ping` | `string` | A space-separated list of URLs to ping when the link is followed. See MDN. | | `referrerPolicy` | `HTMLAttributeReferrerPolicy` | How much of the referrer to send when following the link. See MDN. | | `routerOptions` | ` RouterOptions ` | Options for the configured client side router. | Events | Name | Type | Description | | --- | --- | --- | | `onAction` | `() => void` | Handler that is called when a user performs an action on the item. The exact user event depends on the collection's `selectionBehavior` prop and the interaction modality. | | `onHoverStart` | `( (e: HoverEvent )) => void` | Handler that is called when a hover interaction starts. | | `onHoverEnd` | `( (e: HoverEvent )) => void` | Handler that is called when a hover interaction ends. | | `onHoverChange` | `( (isHovering: boolean )) => void` | Handler that is called when the hover state changes. | ## Styling# * * * React Aria components can be styled in many ways, including using CSS classes, inline styles, utility classes (e.g. Tailwind), CSS-in-JS (e.g. Styled Components), etc. By default, all components include a builtin `className` attribute which can be targeted using CSS selectors. These follow the `react-aria-ComponentName` naming convention. .react-aria-GridList { /* ... */ } .react-aria-GridList { /* ... */ } .react-aria-GridList { /* ... */ } A custom `className` can also be specified on any component. This overrides the default `className` provided by React Aria with your own. <GridList className="my-gridlist"> {/* ... */} </GridList> <GridList className="my-gridlist"> {/* ... */} </GridList> <GridList className="my-gridlist"> {/* ... */} </GridList> In addition, some components support multiple UI states (e.g. pressed, hovered, etc.). React Aria components expose states using data attributes, which you can target in CSS selectors. For example: .react-aria-GridListItem[data-selected] { /* ... */ } .react-aria-GridListItem[data-focused] { /* ... */ } .react-aria-GridListItem[data-selected] { /* ... */ } .react-aria-GridListItem[data-focused] { /* ... */ } .react-aria-GridListItem[data-selected] { /* ... */ } .react-aria-GridListItem[data-focused] { /* ... */ } The `className` and `style` props also accept functions which receive states for styling. This lets you dynamically determine the classes or styles to apply, which is useful when using utility CSS libraries like Tailwind. <GridListItem className={({ isSelected }) => isSelected ? 'bg-blue-400' : 'bg-gray-100'} > Item </GridListItem> <GridListItem className={({ isSelected }) => isSelected ? 'bg-blue-400' : 'bg-gray-100'} > Item </GridListItem> <GridListItem className={( { isSelected } ) => isSelected ? 'bg-blue-400' : 'bg-gray-100'} > Item </GridListItem> 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 checkbox only when selection is enabled. <GridListItem> {({selectionMode}) => ( <> {selectionMode !== 'none' && <Checkbox />} Item </> )} </GridListItem> <GridListItem> {({selectionMode}) => ( <> {selectionMode !== 'none' && <Checkbox />} Item </> )} </GridListItem> <GridListItem> {( { selectionMode } ) => ( <> {selectionMode !== 'none' && ( <Checkbox /> )} Item </> )} </GridListItem> The states and selectors for each component used in a `GridList` are documented below. ### GridList# A `GridList` can be targeted with the `.react-aria-GridList` CSS selector, or by overriding with a custom `className`. It supports the following states and render props: | Name | CSS Selector | Description | | --- | --- | --- | | `isEmpty` | `[data-empty]` | Whether the list has no items and should display its empty state. | | `isFocused` | `[data-focused]` | Whether the grid list is currently focused. | | `isFocusVisible` | `[data-focus-visible]` | Whether the grid list is currently keyboard focused. | | `isDropTarget` | `[data-drop-target]` | Whether the grid list is currently the active drop target. | | `layout` | `[data-layout="stack | grid"]` | Whether the items are arranged in a stack or grid. | | `state` | `—` | State of the grid list. | ### GridListItem# A `GridListItem` can be targeted with the `.react-aria-GridListItem` CSS selector, or by overriding with a custom `className`. It supports the following states and render props: | Name | CSS Selector | Description | | --- | --- | --- | | `isHovered` | `[data-hovered]` | Whether the item is currently hovered with a mouse. | | `isPressed` | `[data-pressed]` | Whether the item is currently in a pressed state. | | `isSelected` | `[data-selected]` | Whether the item is currently selected. | | `isFocused` | `[data-focused]` | Whether the item is currently focused. | | `isFocusVisible` | `[data-focus-visible]` | Whether the item is currently keyboard focused. | | `isDisabled` | `[data-disabled]` | Whether the item is non-interactive, i.e. both selection and actions are disabled and the item may not be focused. Dependent on `disabledKeys` and `disabledBehavior`. | | `selectionMode` | `[data-selection-mode="single | multiple"]` | The type of selection that is allowed in the collection. | | `selectionBehavior` | `—` | The selection behavior for the collection. | | `allowsDragging` | `[data-allows-dragging]` | Whether the item allows dragging. | | `isDragging` | `[data-dragging]` | Whether the item is currently being dragged. | | `isDropTarget` | `[data-drop-target]` | Whether the item is currently an active drop target. | ## Advanced customization# * * * ### Composition# If you need to customize one of the components within a `GridList`, such as `GridListItem`, in many cases you can create a wrapper component. This lets you customize the props passed to the component. function MyItem(props) { return <GridListItem {...props} className="my-item" /> } function MyItem(props) { return <GridListItem {...props} className="my-item" /> } function MyItem(props) { return ( <GridListItem {...props} className="my-item" /> ); } ### Contexts# All React Aria Components export a corresponding context that can be used to send props to them from a parent element. This enables you to build your own compositional APIs similar to those found in React Aria Components itself. You can send any prop or ref via context that you could pass to the corresponding component. The local props and ref on the component are merged with the ones passed via context, with the local props taking precedence (following the rules documented in mergeProps). | Component | Context | Props | Ref | | --- | --- | --- | --- | | `GridList` | `GridListContext` | ` GridListProps ` | `HTMLDivElement` | This example shows a component that accepts a `GridList` and a ToggleButton as children, and allows the user to turn selection mode for the list on and off by pressing the button. import type {SelectionMode} from 'react-aria-components'; import {ToggleButtonContext, GridListContext} from 'react-aria-components'; function Selectable({children}) { let [isSelected, onChange] = React.useState(false); let selectionMode: SelectionMode = isSelected ? 'multiple' : 'none'; return ( <ToggleButtonContext.Provider value={{isSelected, onChange}}> <GridListContext.Provider value={{selectionMode}}> {children} </GridListContext.Provider> </ToggleButtonContext.Provider> ); } import type {SelectionMode} from 'react-aria-components'; import { GridListContext, ToggleButtonContext } from 'react-aria-components'; function Selectable({ children }) { let [isSelected, onChange] = React.useState(false); let selectionMode: SelectionMode = isSelected ? 'multiple' : 'none'; return ( <ToggleButtonContext.Provider value={{ isSelected, onChange }} > <GridListContext.Provider value={{ selectionMode }}> {children} </GridListContext.Provider> </ToggleButtonContext.Provider> ); } import type {SelectionMode} from 'react-aria-components'; import { GridListContext, ToggleButtonContext } from 'react-aria-components'; function Selectable( { children } ) { let [ isSelected, onChange ] = React.useState( false ); let selectionMode: SelectionMode = isSelected ? 'multiple' : 'none'; return ( <ToggleButtonContext.Provider value={{ isSelected, onChange }} > <GridListContext.Provider value={{ selectionMode }} > {children} </GridListContext.Provider> </ToggleButtonContext.Provider> ); } The `Selectable` component can be reused to make the selection mode of any nested `GridList` controlled by a `ToggleButton`. import {ToggleButton} from 'react-aria-components'; <Selectable> <ToggleButton style={{marginBottom: '8px'}}>Select</ToggleButton> <GridList aria-label="Ice cream flavors"> <MyItem>Chocolate</MyItem> <MyItem>Mint</MyItem> <MyItem>Strawberry</MyItem> <MyItem>Vanilla</MyItem> </GridList> </Selectable> import {ToggleButton} from 'react-aria-components'; <Selectable> <ToggleButton style={{ marginBottom: '8px' }}> Select </ToggleButton> <GridList aria-label="Ice cream flavors"> <MyItem>Chocolate</MyItem> <MyItem>Mint</MyItem> <MyItem>Strawberry</MyItem> <MyItem>Vanilla</MyItem> </GridList> </Selectable> import {ToggleButton} from 'react-aria-components'; <Selectable> <ToggleButton style={{ marginBottom: '8px' }} > Select </ToggleButton> <GridList aria-label="Ice cream flavors"> <MyItem> Chocolate </MyItem> <MyItem> Mint </MyItem> <MyItem> Strawberry </MyItem> <MyItem> Vanilla </MyItem> </GridList> </Selectable> Select Chocolate Mint Strawberry Vanilla ### Custom children# GridList passes props to its child components, such as the selection checkboxes, via their associated contexts. These contexts are exported so you can also consume them in your own custom components. This enables you to reuse existing components from your app or component library together with React Aria Components. | Component | Context | Props | Ref | | --- | --- | --- | --- | | `Checkbox` | `CheckboxContext` | ` CheckboxProps ` | `HTMLLabelElement` | | `Button` | `ButtonContext` | ` ButtonProps ` | `HTMLButtonElement` | | `Text` | `TextContext` | ` TextProps ` | `HTMLElement` | This example consumes from `CheckboxContext` in an existing styled checkbox component to make it compatible with React Aria Components. The `useContextProps` hook merges the local props and ref with the ones provided via context by GridList. See useCheckbox for more details about the hooks used in this example. import type {CheckboxProps} from 'react-aria-components'; import {CheckboxContext, useContextProps} from 'react-aria-components'; import {useToggleState} from 'react-stately'; import {useCheckbox} from 'react-aria'; const MyCustomCheckbox = React.forwardRef( (props: CheckboxProps, ref: React.ForwardedRef<HTMLInputElement>) => { // Merge the local props and ref with the ones provided via context. [props, ref] = useContextProps(props, ref, CheckboxContext); let state = useToggleState(props); let { inputProps } = useCheckbox(props, state, ref); return <input {...inputProps} ref={ref} />; } ); import type {CheckboxProps} from 'react-aria-components'; import { CheckboxContext, useContextProps } from 'react-aria-components'; import {useToggleState} from 'react-stately'; import {useCheckbox} from 'react-aria'; const MyCustomCheckbox = React.forwardRef( ( props: CheckboxProps, ref: React.ForwardedRef<HTMLInputElement> ) => { // Merge the local props and ref with the ones provided via context. [props, ref] = useContextProps( props, ref, CheckboxContext ); let state = useToggleState(props); let { inputProps } = useCheckbox(props, state, ref); return <input {...inputProps} ref={ref} />; } ); import type {CheckboxProps} from 'react-aria-components'; import { CheckboxContext, useContextProps } from 'react-aria-components'; import {useToggleState} from 'react-stately'; import {useCheckbox} from 'react-aria'; const MyCustomCheckbox = React.forwardRef( ( props: CheckboxProps, ref: React.ForwardedRef< HTMLInputElement > ) => { // Merge the local props and ref with the ones provided via context. [props, ref] = useContextProps( props, ref, CheckboxContext ); let state = useToggleState( props ); let { inputProps } = useCheckbox( props, state, ref ); return ( <input {...inputProps} ref={ref} /> ); } ); Now you can use `MyCustomCheckbox` within a `GridList`, in place of the builtin React Aria Components `Checkbox`. <GridList> <GridListItem> <MyCustomCheckbox slot="selection" /> {/* ... */} </GridListItem> </GridList> <GridList> <GridListItem> <MyCustomCheckbox slot="selection" /> {/* ... */} </GridListItem> </GridList> <GridList> <GridListItem> <MyCustomCheckbox slot="selection" /> {/* ... */} </GridListItem> </GridList> ### Hooks# If you need to customize things even further, such as accessing internal state, intercepting events, or customizing DOM structure, you can drop down to the lower level Hook-based API. See useGridList for more details. ## Testing# * * * ### Test utils alpha# `@react-aria/test-utils` offers common gridlist interaction utilities which you may find helpful when writing tests. See here for more information on how to setup these utilities in your tests. Below is the full definition of the gridlist tester and a sample of how you could use it in your test suite. // GridList.test.ts import {render, within} from '@testing-library/react'; import {User} from '@react-aria/test-utils'; let testUtilUser = new User({ interactionType: 'mouse' }); // ... it('GridList can select a row via keyboard', async function () { // Render your test component/app and initialize the gridlist tester let { getByTestId } = render( <GridList data-testid="test-gridlist" selectionMode="single"> ... </GridList> ); let gridListTester = testUtilUser.createTester('GridList', { root: getByTestId('test-gridlist'), interactionType: 'keyboard' }); let row = gridListTester.rows[0]; expect(within(row).getByRole('checkbox')).not.toBeChecked(); expect(gridListTester.selectedRows).toHaveLength(0); await gridListTester.toggleRowSelection({ row: 0 }); expect(within(row).getByRole('checkbox')).toBeChecked(); expect(gridListTester.selectedRows).toHaveLength(1); await gridListTester.toggleRowSelection({ row: 0 }); expect(within(row).getByRole('checkbox')).not.toBeChecked(); expect(gridListTester.selectedRows).toHaveLength(0); }); // GridList.test.ts import {render, within} from '@testing-library/react'; import {User} from '@react-aria/test-utils'; let testUtilUser = new User({ interactionType: 'mouse' }); // ... it('GridList can select a row via keyboard', async function () { // Render your test component/app and initialize the gridlist tester let { getByTestId } = render( <GridList data-testid="test-gridlist" selectionMode="single" > ... </GridList> ); let gridListTester = testUtilUser.createTester( 'GridList', { root: getByTestId('test-gridlist'), interactionType: 'keyboard' } ); let row = gridListTester.rows[0]; expect(within(row).getByRole('checkbox')).not .toBeChecked(); expect(gridListTester.selectedRows).toHaveLength(0); await gridListTester.toggleRowSelection({ row: 0 }); expect(within(row).getByRole('checkbox')).toBeChecked(); expect(gridListTester.selectedRows).toHaveLength(1); await gridListTester.toggleRowSelection({ row: 0 }); expect(within(row).getByRole('checkbox')).not .toBeChecked(); expect(gridListTester.selectedRows).toHaveLength(0); }); // GridList.test.ts import { render, within } from '@testing-library/react'; import {User} from '@react-aria/test-utils'; let testUtilUser = new User({ interactionType: 'mouse' }); // ... it('GridList can select a row via keyboard', async function () { // Render your test component/app and initialize the gridlist tester let { getByTestId } = render( <GridList data-testid="test-gridlist" selectionMode="single" > ... </GridList> ); let gridListTester = testUtilUser .createTester( 'GridList', { root: getByTestId( 'test-gridlist' ), interactionType: 'keyboard' } ); let row = gridListTester .rows[0]; expect( within(row) .getByRole( 'checkbox' ) ).not.toBeChecked(); expect( gridListTester .selectedRows ).toHaveLength(0); await gridListTester .toggleRowSelection({ row: 0 }); expect( within(row) .getByRole( 'checkbox' ) ).toBeChecked(); expect( gridListTester .selectedRows ).toHaveLength(1); await gridListTester .toggleRowSelection({ row: 0 }); expect( within(row) .getByRole( 'checkbox' ) ).not.toBeChecked(); expect( gridListTester .selectedRows ).toHaveLength(0); }); ### Properties | Name | Type | Description | | --- | --- | --- | | `gridlist` | `HTMLElement` | Returns the gridlist. | | `rows` | `HTMLElement[]` | Returns the gridlist's rows if any. | | `selectedRows` | `HTMLElement[]` | Returns the gridlist's selected rows if any. | ### Methods | Method | Description | | --- | --- | | `constructor( (opts: GridListTesterOpts )): void` | | | `setInteractionType( (type: UserOpts ['interactionType'] )): void` | Set the interaction type used by the gridlist tester. | | `findRow( (opts: { rowIndexOrText: number | | string } )): HTMLElement` | Returns a row matching the specified index or text content. | | `toggleRowSelection( (opts: GridListToggleRowOpts )): Promise<void>` | Toggles the selection for the specified gridlist row. Defaults to using the interaction type set on the gridlist tester. Note that this will endevor to always add/remove JUST the provided row to the set of selected rows. | | `triggerRowAction( (opts: GridListRowActionOpts )): Promise<void>` | Triggers the action for the specified gridlist row. Defaults to using the interaction type set on the gridlist tester. | | `cells( (opts: { element?: HTMLElement } )): HTMLElement[]` | Returns the gridlist's cells if any. Can be filtered against a specific row if provided via `element`. | --- ## Page: https://react-spectrum.adobe.com/react-aria/ListBox.html # ListBox A listbox displays a list of options and allows a user to select one or more of them. <table><tbody><tr><th>install</th><td><code>yarn add react-aria-components</code></td></tr><tr><th>version</th><td>1.10.1</td></tr><tr><th>usage</th><td><code><span>import</span> {ListBox} <span>from</span> <span>'react-aria-components'</span></code></td></tr></tbody></table> View ARIA pattern W3C View repository GitHub View package NPM ## Example# * * * import {ListBox, ListBoxItem} from 'react-aria-components'; <ListBox aria-label="Favorite animal" selectionMode="single"> <ListBoxItem>Aardvark</ListBoxItem> <ListBoxItem>Cat</ListBoxItem> <ListBoxItem>Dog</ListBoxItem> <ListBoxItem>Kangaroo</ListBoxItem> <ListBoxItem>Panda</ListBoxItem> <ListBoxItem>Snake</ListBoxItem> </ListBox> import {ListBox, ListBoxItem} from 'react-aria-components'; <ListBox aria-label="Favorite animal" selectionMode="single" > <ListBoxItem>Aardvark</ListBoxItem> <ListBoxItem>Cat</ListBoxItem> <ListBoxItem>Dog</ListBoxItem> <ListBoxItem>Kangaroo</ListBoxItem> <ListBoxItem>Panda</ListBoxItem> <ListBoxItem>Snake</ListBoxItem> </ListBox> import { ListBox, ListBoxItem } from 'react-aria-components'; <ListBox aria-label="Favorite animal" selectionMode="single" > <ListBoxItem> Aardvark </ListBoxItem> <ListBoxItem> Cat </ListBoxItem> <ListBoxItem> Dog </ListBoxItem> <ListBoxItem> Kangaroo </ListBoxItem> <ListBoxItem> Panda </ListBoxItem> <ListBoxItem> Snake </ListBoxItem> </ListBox> Aardvark Cat Dog Kangaroo Panda Snake Show CSS @import "@react-aria/example-theme"; .react-aria-ListBox { display: flex; flex-direction: column; gap: 4px; max-height: inherit; overflow: auto; padding: 4px; border: 1px solid var(--border-color); border-radius: 6px; background: var(--overlay-background); forced-color-adjust: none; outline: none; width: 250px; max-height: 300px; min-height: 100px; box-sizing: border-box; &[data-focus-visible] { outline: 2px solid var(--focus-ring-color); outline-offset: -1px; } } .react-aria-ListBoxItem { padding: 0 0.571rem; border-radius: 6px; outline: none; cursor: default; color: var(--text-color); font-size: 1.072rem; position: relative; display: flex; flex-direction: column; justify-content: center; min-height: 32px; &[data-focus-visible] { outline: 2px solid var(--focus-ring-color); outline-offset: -2px; } &[data-selected] { background: var(--highlight-background); color: var(--highlight-foreground); &[data-focus-visible] { outline-color: var(--highlight-foreground); outline-offset: -4px; } } } @import "@react-aria/example-theme"; .react-aria-ListBox { display: flex; flex-direction: column; gap: 4px; max-height: inherit; overflow: auto; padding: 4px; border: 1px solid var(--border-color); border-radius: 6px; background: var(--overlay-background); forced-color-adjust: none; outline: none; width: 250px; max-height: 300px; min-height: 100px; box-sizing: border-box; &[data-focus-visible] { outline: 2px solid var(--focus-ring-color); outline-offset: -1px; } } .react-aria-ListBoxItem { padding: 0 0.571rem; border-radius: 6px; outline: none; cursor: default; color: var(--text-color); font-size: 1.072rem; position: relative; display: flex; flex-direction: column; justify-content: center; min-height: 32px; &[data-focus-visible] { outline: 2px solid var(--focus-ring-color); outline-offset: -2px; } &[data-selected] { background: var(--highlight-background); color: var(--highlight-foreground); &[data-focus-visible] { outline-color: var(--highlight-foreground); outline-offset: -4px; } } } @import "@react-aria/example-theme"; .react-aria-ListBox { display: flex; flex-direction: column; gap: 4px; max-height: inherit; overflow: auto; padding: 4px; border: 1px solid var(--border-color); border-radius: 6px; background: var(--overlay-background); forced-color-adjust: none; outline: none; width: 250px; max-height: 300px; min-height: 100px; box-sizing: border-box; &[data-focus-visible] { outline: 2px solid var(--focus-ring-color); outline-offset: -1px; } } .react-aria-ListBoxItem { padding: 0 0.571rem; border-radius: 6px; outline: none; cursor: default; color: var(--text-color); font-size: 1.072rem; position: relative; display: flex; flex-direction: column; justify-content: center; min-height: 32px; &[data-focus-visible] { outline: 2px solid var(--focus-ring-color); outline-offset: -2px; } &[data-selected] { background: var(--highlight-background); color: var(--highlight-foreground); &[data-focus-visible] { outline-color: var(--highlight-foreground); outline-offset: -4px; } } } ## Features# * * * A listbox can be built using the <select> and <option> HTML elements, but this is not possible to style consistently cross browser. `ListBox` helps you build accessible listbox components that can be styled as needed. * **Item selection** – Single or multiple selection, disabled rows, and both `toggle` and `replace` selection behaviors. * **Keyboard navigation** – List items can be navigated using the arrow keys, along with page up/down, home/end, etc. Typeahead, auto scrolling, and selection modifier keys are supported as well. * **Layout options** – Items can be arranged in a vertical or horizontal stack, or as a two-dimensional grid. * **Drag and drop** – ListBox supports drag and drop to reorder, insert, or update items via mouse, touch, keyboard, and screen reader interactions. * **Virtualized scrolling** – Use Virtualizer to improve performance of large lists by rendering only the visible items. * **Touch friendly** – Selection behavior adapts depending on the device. For example, selection occurs on mouse down but on touch up, which is consistent with native conventions. * **Accessible** – Follows the ARIA listbox pattern, with support for items and sections, and slots for label and description elements within each item for improved screen reader announcement. * **Styleable** – Items include builtin states for styling, such as hover, press, focus, selected, and disabled. Note: `ListBox` only handles the list itself. For a dropdown, see Select. ## Anatomy# * * * A listbox consists of a container element, with a list of items or sections inside. Users can select one or more items by clicking, tapping, or navigating with the keyboard. import {Header, ListBox, ListBoxItem, ListBoxSection, Text} from 'react-aria-components'; <ListBox> <ListBoxItem> <Text slot="label" /> <Text slot="description" /> </ListBoxItem> <ListBoxSection> <Header /> <ListBoxItem /> </ListBoxSection> </ListBox> import { Header, ListBox, ListBoxItem, ListBoxSection, Text } from 'react-aria-components'; <ListBox> <ListBoxItem> <Text slot="label" /> <Text slot="description" /> </ListBoxItem> <ListBoxSection> <Header /> <ListBoxItem /> </ListBoxSection> </ListBox> import { Header, ListBox, ListBoxItem, ListBoxSection, Text } from 'react-aria-components'; <ListBox> <ListBoxItem> <Text slot="label" /> <Text slot="description" /> </ListBoxItem> <ListBoxSection> <Header /> <ListBoxItem /> </ListBoxSection> </ListBox> ### Concepts# `ListBox` makes use of the following concepts: Collections Defining collections of items, async loading, and updating items over time. Selection Interactions and data structures to represent selection. Drag and drop Concepts and interactions for an accessible drag and drop experience. ## Examples# * * * Contact List A ListBox featuring sticky section headers and multiple selection. Image Grid An async image gallery with selectable items, styled with Tailwind CSS. Searchable Select A Select component with Autocomplete filtering. ## Starter kits# * * * To help kick-start your project, we offer starter kits that include example implementations of all React Aria components with various styling solutions. All components are fully styled, including support for dark mode, high contrast mode, and all UI states. Each starter comes with a pre-configured Storybook that you can experiment with, or use as a starting point for your own component library. Vanilla CSS Download ZIP Preview Tailwind CSS Download ZIP Preview ## Reusable wrappers# * * * If you will use a ListBox in multiple places in your app, you can wrap all of the pieces into a reusable component. This way, the DOM structure, styling code, and other logic are defined in a single place and reused everywhere to ensure consistency. This example wraps `ListBox` and all of its children together into a single component which accepts a `label` prop and `children`, which are passed through to the right places. The `ListBoxItem` component is also wrapped to apply class names based on the current state, as described in the styling section. import type {ListBoxItemProps, ListBoxProps} from 'react-aria-components'; function MyListBox<T extends object>({ children, ...props }: ListBoxProps<T>) { return ( <ListBox {...props} className="my-listbox"> {children} </ListBox> ); } function MyItem(props: ListBoxItemProps) { return ( <ListBoxItem {...props} className={({ isFocusVisible, isSelected }) => `my-item ${isFocusVisible ? 'focused' : ''} ${ isSelected ? 'selected' : '' }`} /> ); } <MyListBox aria-label="Ice cream flavor" selectionMode="single"> <MyItem>Chocolate</MyItem> <MyItem>Mint</MyItem> <MyItem>Strawberry</MyItem> <MyItem>Vanilla</MyItem> </MyListBox> import type { ListBoxItemProps, ListBoxProps } from 'react-aria-components'; function MyListBox<T extends object>( { children, ...props }: ListBoxProps<T> ) { return ( <ListBox {...props} className="my-listbox"> {children} </ListBox> ); } function MyItem(props: ListBoxItemProps) { return ( <ListBoxItem {...props} className={({ isFocusVisible, isSelected }) => `my-item ${isFocusVisible ? 'focused' : ''} ${ isSelected ? 'selected' : '' }`} /> ); } <MyListBox aria-label="Ice cream flavor" selectionMode="single" > <MyItem>Chocolate</MyItem> <MyItem>Mint</MyItem> <MyItem>Strawberry</MyItem> <MyItem>Vanilla</MyItem> </MyListBox> import type { ListBoxItemProps, ListBoxProps } from 'react-aria-components'; function MyListBox< T extends object >( { children, ...props }: ListBoxProps<T> ) { return ( <ListBox {...props} className="my-listbox" > {children} </ListBox> ); } function MyItem( props: ListBoxItemProps ) { return ( <ListBoxItem {...props} className={( { isFocusVisible, isSelected } ) => `my-item ${ isFocusVisible ? 'focused' : '' } ${ isSelected ? 'selected' : '' }`} /> ); } <MyListBox aria-label="Ice cream flavor" selectionMode="single" > <MyItem> Chocolate </MyItem> <MyItem>Mint</MyItem> <MyItem> Strawberry </MyItem> <MyItem> Vanilla </MyItem> </MyListBox> Chocolate Mint Strawberry Vanilla Show CSS .my-listbox { max-height: inherit; overflow: auto; padding: 4px; gap: 4px; border: 1px solid var(--border-color); border-radius: 6px; background: var(--overlay-background); outline: none; max-width: 250px; max-height: 300px; box-sizing: border-box; } .my-item { --highlight: #e70073; padding: 0.286rem 0.571rem; border-radius: 6px; outline: none; cursor: default; color: var(--text-color); font-size: 1.072rem; &.selected { background: var(--highlight); color: var(--highlight-foreground); } &.focused { outline: 2px solid var(--highlight); outline-offset: 2px; } } @media (forced-colors: active) { .my-item { forced-color-adjust: none; --highlight: Highlight; } } .my-listbox { max-height: inherit; overflow: auto; padding: 4px; gap: 4px; border: 1px solid var(--border-color); border-radius: 6px; background: var(--overlay-background); outline: none; max-width: 250px; max-height: 300px; box-sizing: border-box; } .my-item { --highlight: #e70073; padding: 0.286rem 0.571rem; border-radius: 6px; outline: none; cursor: default; color: var(--text-color); font-size: 1.072rem; &.selected { background: var(--highlight); color: var(--highlight-foreground); } &.focused { outline: 2px solid var(--highlight); outline-offset: 2px; } } @media (forced-colors: active) { .my-item { forced-color-adjust: none; --highlight: Highlight; } } .my-listbox { max-height: inherit; overflow: auto; padding: 4px; gap: 4px; border: 1px solid var(--border-color); border-radius: 6px; background: var(--overlay-background); outline: none; max-width: 250px; max-height: 300px; box-sizing: border-box; } .my-item { --highlight: #e70073; padding: 0.286rem 0.571rem; border-radius: 6px; outline: none; cursor: default; color: var(--text-color); font-size: 1.072rem; &.selected { background: var(--highlight); color: var(--highlight-foreground); } &.focused { outline: 2px solid var(--highlight); outline-offset: 2px; } } @media (forced-colors: active) { .my-item { forced-color-adjust: none; --highlight: Highlight; } } ## Content# * * * `ListBox` follows the Collection Components API, accepting both static and dynamic collections. The examples above show static collections, which can be used when the full list of options is known ahead of time. Dynamic collections, as shown below, can be used when the options come from an external data source such as an API call, or update over time. As seen below, an iterable list of options is passed to the ListBox using the `items` prop. Each item accepts an `id` prop, which is passed to the `onSelectionChange` handler to identify the selected item. Alternatively, if the item objects contain an `id` property, as shown in the example below, then this is used automatically and an `id` prop is not required. function Example() { let options = [ { id: 1, name: 'Aardvark' }, { id: 2, name: 'Cat' }, { id: 3, name: 'Dog' }, { id: 4, name: 'Kangaroo' }, { id: 5, name: 'Koala' }, { id: 6, name: 'Penguin' }, { id: 7, name: 'Snake' }, { id: 8, name: 'Turtle' }, { id: 9, name: 'Wombat' } ]; return ( <ListBox aria-label="Animals" items={options} selectionMode="single"> {(item) => <ListBoxItem>{item.name}</ListBoxItem>} </ListBox> ); } function Example() { let options = [ { id: 1, name: 'Aardvark' }, { id: 2, name: 'Cat' }, { id: 3, name: 'Dog' }, { id: 4, name: 'Kangaroo' }, { id: 5, name: 'Koala' }, { id: 6, name: 'Penguin' }, { id: 7, name: 'Snake' }, { id: 8, name: 'Turtle' }, { id: 9, name: 'Wombat' } ]; return ( <ListBox aria-label="Animals" items={options} selectionMode="single" > {(item) => <ListBoxItem>{item.name}</ListBoxItem>} </ListBox> ); } function Example() { let options = [ { id: 1, name: 'Aardvark' }, { id: 2, name: 'Cat' }, { id: 3, name: 'Dog' }, { id: 4, name: 'Kangaroo' }, { id: 5, name: 'Koala' }, { id: 6, name: 'Penguin' }, { id: 7, name: 'Snake' }, { id: 8, name: 'Turtle' }, { id: 9, name: 'Wombat' } ]; return ( <ListBox aria-label="Animals" items={options} selectionMode="single" > {(item) => ( <ListBoxItem> {item.name} </ListBoxItem> )} </ListBox> ); } Aardvark Cat Dog Kangaroo Koala Penguin Snake Turtle Wombat ## Selection# * * * ListBox supports multiple selection modes. By default, selection is disabled, however this can be changed using the `selectionMode` prop. Use `defaultSelectedKeys` to provide a default set of selected items (uncontrolled) and `selectedKeys` to set the selected items (controlled). The value of the selected keys must match the `id` prop of the items. See the Selection guide for more details. import type {Selection} from 'react-aria-components'; function Example() { let [selected, setSelected] = React.useState<Selection>(new Set(['cheese'])); return ( <> <ListBox aria-label="Sandwich contents" selectionMode="multiple" selectedKeys={selected} onSelectionChange={setSelected} > <ListBoxItem id="lettuce">Lettuce</ListBoxItem> <ListBoxItem id="tomato">Tomato</ListBoxItem> <ListBoxItem id="cheese">Cheese</ListBoxItem> <ListBoxItem id="tuna">Tuna Salad</ListBoxItem> <ListBoxItem id="egg">Egg Salad</ListBoxItem> <ListBoxItem id="ham">Ham</ListBoxItem> </ListBox> <p> Current selection (controlled):{' '} {selected === 'all' ? 'all' : [...selected].join(', ')} </p> </> ); } import type {Selection} from 'react-aria-components'; function Example() { let [selected, setSelected] = React.useState<Selection>( new Set(['cheese']) ); return ( <> <ListBox aria-label="Sandwich contents" selectionMode="multiple" selectedKeys={selected} onSelectionChange={setSelected} > <ListBoxItem id="lettuce">Lettuce</ListBoxItem> <ListBoxItem id="tomato">Tomato</ListBoxItem> <ListBoxItem id="cheese">Cheese</ListBoxItem> <ListBoxItem id="tuna">Tuna Salad</ListBoxItem> <ListBoxItem id="egg">Egg Salad</ListBoxItem> <ListBoxItem id="ham">Ham</ListBoxItem> </ListBox> <p> Current selection (controlled): {selected === 'all' ? 'all' : [...selected].join(', ')} </p> </> ); } import type {Selection} from 'react-aria-components'; function Example() { let [ selected, setSelected ] = React.useState< Selection >(new Set(['cheese'])); return ( <> <ListBox aria-label="Sandwich contents" selectionMode="multiple" selectedKeys={selected} onSelectionChange={setSelected} > <ListBoxItem id="lettuce"> Lettuce </ListBoxItem> <ListBoxItem id="tomato"> Tomato </ListBoxItem> <ListBoxItem id="cheese"> Cheese </ListBoxItem> <ListBoxItem id="tuna"> Tuna Salad </ListBoxItem> <ListBoxItem id="egg"> Egg Salad </ListBoxItem> <ListBoxItem id="ham"> Ham </ListBoxItem> </ListBox> <p> Current selection (controlled): {' '} {selected === 'all' ? 'all' : [...selected] .join(', ')} </p> </> ); } Lettuce Tomato Cheese Tuna Salad Egg Salad Ham Current selection (controlled): cheese ### Selection behavior# By default, `ListBox` uses the `"toggle"` selection behavior, which behaves like a checkbox group: clicking, tapping, or pressing the Space or Enter keys toggles selection for the focused row. Using the arrow keys moves focus but does not change selection. When `selectionBehavior` is set to `"replace"`, clicking a row with the mouse replaces the selection with only that row. Using the arrow keys moves both focus and selection. To select multiple rows, modifier keys such as Ctrl, Cmd, and Shift can be used. On touch screen devices, selection always behaves as toggle since modifier keys may not be available. These selection behaviors are defined in Aria Practices. <ListBox aria-label="Sandwich contents" selectionMode="multiple" selectionBehavior="replace"> <ListBoxItem id="lettuce">Lettuce</ListBoxItem> <ListBoxItem id="tomato">Tomato</ListBoxItem> <ListBoxItem id="cheese">Cheese</ListBoxItem> <ListBoxItem id="tuna">Tuna Salad</ListBoxItem> <ListBoxItem id="egg">Egg Salad</ListBoxItem> <ListBoxItem id="ham">Ham</ListBoxItem> </ListBox> <ListBox aria-label="Sandwich contents" selectionMode="multiple" selectionBehavior="replace"> <ListBoxItem id="lettuce">Lettuce</ListBoxItem> <ListBoxItem id="tomato">Tomato</ListBoxItem> <ListBoxItem id="cheese">Cheese</ListBoxItem> <ListBoxItem id="tuna">Tuna Salad</ListBoxItem> <ListBoxItem id="egg">Egg Salad</ListBoxItem> <ListBoxItem id="ham">Ham</ListBoxItem> </ListBox> <ListBox aria-label="Sandwich contents" selectionMode="multiple" selectionBehavior="replace"> <ListBoxItem id="lettuce"> Lettuce </ListBoxItem> <ListBoxItem id="tomato"> Tomato </ListBoxItem> <ListBoxItem id="cheese"> Cheese </ListBoxItem> <ListBoxItem id="tuna"> Tuna Salad </ListBoxItem> <ListBoxItem id="egg"> Egg Salad </ListBoxItem> <ListBoxItem id="ham"> Ham </ListBoxItem> </ListBox> Lettuce Tomato Cheese Tuna Salad Egg Salad Ham ## Links# * * * By default, interacting with an item in a ListBox triggers `onSelectionChange`. Alternatively, items may be links to another page or website. This can be achieved by passing the `href` prop to the `<ListBoxItem>` component. <ListBox aria-label="Links"> <ListBoxItem href="https://adobe.com/" target="_blank">Adobe</ListBoxItem> <ListBoxItem href="https://apple.com/" target="_blank">Apple</ListBoxItem> <ListBoxItem href="https://google.com/" target="_blank">Google</ListBoxItem> <ListBoxItem href="https://microsoft.com/" target="_blank"> Microsoft </ListBoxItem> </ListBox> <ListBox aria-label="Links"> <ListBoxItem href="https://adobe.com/" target="_blank"> Adobe </ListBoxItem> <ListBoxItem href="https://apple.com/" target="_blank"> Apple </ListBoxItem> <ListBoxItem href="https://google.com/" target="_blank"> Google </ListBoxItem> <ListBoxItem href="https://microsoft.com/" target="_blank" > Microsoft </ListBoxItem> </ListBox> <ListBox aria-label="Links"> <ListBoxItem href="https://adobe.com/" target="_blank" > Adobe </ListBoxItem> <ListBoxItem href="https://apple.com/" target="_blank" > Apple </ListBoxItem> <ListBoxItem href="https://google.com/" target="_blank" > Google </ListBoxItem> <ListBoxItem href="https://microsoft.com/" target="_blank" > Microsoft </ListBoxItem> </ListBox> AdobeAppleGoogleMicrosoft By default, link items in a ListBox are not selectable, and only perform navigation when the user interacts with them. However, with the "replace" selection behavior, items will be selected when single clicking or pressing the Space key, and navigate to the link when double clicking or pressing the Enter key. <ListBox aria-label="Links" selectionMode="multiple" selectionBehavior="replace" > <ListBoxItem href="https://adobe.com/" target="_blank">Adobe</ListBoxItem> <ListBoxItem href="https://apple.com/" target="_blank">Apple</ListBoxItem> <ListBoxItem href="https://google.com/" target="_blank">Google</ListBoxItem> <ListBoxItem href="https://microsoft.com/" target="_blank"> Microsoft </ListBoxItem> </ListBox> <ListBox aria-label="Links" selectionMode="multiple" selectionBehavior="replace" > <ListBoxItem href="https://adobe.com/" target="_blank"> Adobe </ListBoxItem> <ListBoxItem href="https://apple.com/" target="_blank"> Apple </ListBoxItem> <ListBoxItem href="https://google.com/" target="_blank"> Google </ListBoxItem> <ListBoxItem href="https://microsoft.com/" target="_blank" > Microsoft </ListBoxItem> </ListBox> <ListBox aria-label="Links" selectionMode="multiple" selectionBehavior="replace" > <ListBoxItem href="https://adobe.com/" target="_blank" > Adobe </ListBoxItem> <ListBoxItem href="https://apple.com/" target="_blank" > Apple </ListBoxItem> <ListBoxItem href="https://google.com/" target="_blank" > Google </ListBoxItem> <ListBoxItem href="https://microsoft.com/" target="_blank" > Microsoft </ListBoxItem> </ListBox> AdobeAppleGoogleMicrosoft ### Client side routing# The `<ListBoxItem>` component works with frameworks and client side routers like Next.js and React Router. As with other React Aria components that support links, this works via the `RouterProvider` component at the root of your app. See the client side routing guide to learn how to set this up. ## Sections# * * * ListBox supports sections in order to group options. Sections can be used by wrapping groups of items in a `ListBoxSection` element. A `<Header>` element may also be included to label the section. ### Static items# import {ListBoxSection, Header} from 'react-aria-components'; <ListBox aria-label="Sandwich contents" selectionMode="multiple"> <ListBoxSection> <Header>Veggies</Header> <ListBoxItem id="lettuce">Lettuce</ListBoxItem> <ListBoxItem id="tomato">Tomato</ListBoxItem> <ListBoxItem id="onion">Onion</ListBoxItem> </ListBoxSection> <ListBoxSection> <Header>Protein</Header> <ListBoxItem id="ham">Ham</ListBoxItem> <ListBoxItem id="tuna">Tuna</ListBoxItem> <ListBoxItem id="tofu">Tofu</ListBoxItem> </ListBoxSection> <ListBoxSection> <Header>Condiments</Header> <ListBoxItem id="mayo">Mayonaise</ListBoxItem> <ListBoxItem id="mustard">Mustard</ListBoxItem> <ListBoxItem id="ranch">Ranch</ListBoxItem> </ListBoxSection> </ListBox> import { Header, ListBoxSection } from 'react-aria-components'; <ListBox aria-label="Sandwich contents" selectionMode="multiple" > <ListBoxSection> <Header>Veggies</Header> <ListBoxItem id="lettuce">Lettuce</ListBoxItem> <ListBoxItem id="tomato">Tomato</ListBoxItem> <ListBoxItem id="onion">Onion</ListBoxItem> </ListBoxSection> <ListBoxSection> <Header>Protein</Header> <ListBoxItem id="ham">Ham</ListBoxItem> <ListBoxItem id="tuna">Tuna</ListBoxItem> <ListBoxItem id="tofu">Tofu</ListBoxItem> </ListBoxSection> <ListBoxSection> <Header>Condiments</Header> <ListBoxItem id="mayo">Mayonaise</ListBoxItem> <ListBoxItem id="mustard">Mustard</ListBoxItem> <ListBoxItem id="ranch">Ranch</ListBoxItem> </ListBoxSection> </ListBox> import { Header, ListBoxSection } from 'react-aria-components'; <ListBox aria-label="Sandwich contents" selectionMode="multiple" > <ListBoxSection> <Header> Veggies </Header> <ListBoxItem id="lettuce"> Lettuce </ListBoxItem> <ListBoxItem id="tomato"> Tomato </ListBoxItem> <ListBoxItem id="onion"> Onion </ListBoxItem> </ListBoxSection> <ListBoxSection> <Header> Protein </Header> <ListBoxItem id="ham"> Ham </ListBoxItem> <ListBoxItem id="tuna"> Tuna </ListBoxItem> <ListBoxItem id="tofu"> Tofu </ListBoxItem> </ListBoxSection> <ListBoxSection> <Header> Condiments </Header> <ListBoxItem id="mayo"> Mayonaise </ListBoxItem> <ListBoxItem id="mustard"> Mustard </ListBoxItem> <ListBoxItem id="ranch"> Ranch </ListBoxItem> </ListBoxSection> </ListBox> Veggies Lettuce Tomato Onion Protein Ham Tuna Tofu Condiments Mayonaise Mustard Ranch Show CSS .react-aria-ListBox { .react-aria-ListBoxSection:not(:first-child) { margin-top: 12px; } .react-aria-Header { font-size: 1.143rem; font-weight: bold; padding: 0 0.714rem; } } .react-aria-ListBox { .react-aria-ListBoxSection:not(:first-child) { margin-top: 12px; } .react-aria-Header { font-size: 1.143rem; font-weight: bold; padding: 0 0.714rem; } } .react-aria-ListBox { .react-aria-ListBoxSection:not(:first-child) { margin-top: 12px; } .react-aria-Header { font-size: 1.143rem; font-weight: bold; padding: 0 0.714rem; } } ### Dynamic items# The above example shows sections with static items. Sections can also be populated from a hierarchical data structure. Similarly to the props on ListBox, `<ListBoxSection>` takes an array of data using the `items` prop. If the section also has a header, the `Collection` component can be used to render the child items. import type {Selection} from 'react-aria-components'; import {Collection} from 'react-aria-components'; function Example() { let options = [ {name: 'Australian', children: [ {id: 2, name: 'Koala'}, {id: 3, name: 'Kangaroo'}, {id: 4, name: 'Platypus'} ]}, {name: 'American', children: [ {id: 6, name: 'Bald Eagle'}, {id: 7, name: 'Bison'}, {id: 8, name: 'Skunk'} ]} ]; let [selected, setSelected] = React.useState<Selection>(new Set()); return ( <ListBox aria-label="Pick an animal" items={options} selectedKeys={selected} selectionMode="single" onSelectionChange={setSelected}> {section => ( <ListBoxSection id={section.name}> <Header>{section.name}</Header> <Collection items={section.children}> {item => <ListBoxItem>{item.name}</ListBoxItem>} </Collection> </ListBoxSection> )} </ListBox> ); } import type {Selection} from 'react-aria-components'; import {Collection} from 'react-aria-components'; function Example() { let options = [ { name: 'Australian', children: [ { id: 2, name: 'Koala' }, { id: 3, name: 'Kangaroo' }, { id: 4, name: 'Platypus' } ] }, { name: 'American', children: [ { id: 6, name: 'Bald Eagle' }, { id: 7, name: 'Bison' }, { id: 8, name: 'Skunk' } ] } ]; let [selected, setSelected] = React.useState<Selection>( new Set() ); return ( <ListBox aria-label="Pick an animal" items={options} selectedKeys={selected} selectionMode="single" onSelectionChange={setSelected} > {(section) => ( <ListBoxSection id={section.name}> <Header>{section.name}</Header> <Collection items={section.children}> {(item) => ( <ListBoxItem>{item.name}</ListBoxItem> )} </Collection> </ListBoxSection> )} </ListBox> ); } import type {Selection} from 'react-aria-components'; import {Collection} from 'react-aria-components'; function Example() { let options = [ { name: 'Australian', children: [ { id: 2, name: 'Koala' }, { id: 3, name: 'Kangaroo' }, { id: 4, name: 'Platypus' } ] }, { name: 'American', children: [ { id: 6, name: 'Bald Eagle' }, { id: 7, name: 'Bison' }, { id: 8, name: 'Skunk' } ] } ]; let [ selected, setSelected ] = React.useState< Selection >(new Set()); return ( <ListBox aria-label="Pick an animal" items={options} selectedKeys={selected} selectionMode="single" onSelectionChange={setSelected} > {(section) => ( <ListBoxSection id={section .name} > <Header> {section .name} </Header> <Collection items={section .children} > {(item) => ( <ListBoxItem> {item .name} </ListBoxItem> )} </Collection> </ListBoxSection> )} </ListBox> ); } Australian Koala Kangaroo Platypus American Bald Eagle Bison Skunk ### Accessibility# Sections without a `<Header>` must provide an `aria-label` for accessibility. ## Text slots# * * * By default, items in a `ListBox` are labeled by their text contents for accessibility. Items also support the "label" and "description" slots to separate primary and secondary content, which improves screen reader announcements and can also be used for styling purposes. **Note**: The ARIA spec prohibits listbox items from including interactive content such as buttons, checkboxes, etc. For these cases, see GridList instead. import {Text} from 'react-aria-components'; <ListBox aria-label="Permissions" selectionMode="single"> <ListBoxItem textValue="Read"> <Text slot="label">Read</Text> <Text slot="description">Read only</Text> </ListBoxItem> <ListBoxItem textValue="Write"> <Text slot="label">Write</Text> <Text slot="description">Read and write only</Text> </ListBoxItem> <ListBoxItem textValue="Admin"> <Text slot="label">Admin</Text> <Text slot="description">Full access</Text> </ListBoxItem> </ListBox> import {Text} from 'react-aria-components'; <ListBox aria-label="Permissions" selectionMode="single"> <ListBoxItem textValue="Read"> <Text slot="label">Read</Text> <Text slot="description">Read only</Text> </ListBoxItem> <ListBoxItem textValue="Write"> <Text slot="label">Write</Text> <Text slot="description">Read and write only</Text> </ListBoxItem> <ListBoxItem textValue="Admin"> <Text slot="label">Admin</Text> <Text slot="description">Full access</Text> </ListBoxItem> </ListBox> import {Text} from 'react-aria-components'; <ListBox aria-label="Permissions" selectionMode="single" > <ListBoxItem textValue="Read"> <Text slot="label"> Read </Text> <Text slot="description"> Read only </Text> </ListBoxItem> <ListBoxItem textValue="Write"> <Text slot="label"> Write </Text> <Text slot="description"> Read and write only </Text> </ListBoxItem> <ListBoxItem textValue="Admin"> <Text slot="label"> Admin </Text> <Text slot="description"> Full access </Text> </ListBoxItem> </ListBox> ReadRead only WriteRead and write only AdminFull access Show CSS .react-aria-ListBoxItem { [slot=label] { font-weight: bold; } [slot=description] { font-size: small; } } .react-aria-ListBoxItem { [slot=label] { font-weight: bold; } [slot=description] { font-size: small; } } .react-aria-ListBoxItem { [slot=label] { font-weight: bold; } [slot=description] { font-size: small; } } ## Layouts# * * * By default, ListBox expects items to be arranged in a vertical stack, and implements keyboard navigation and drag and drop accordingly. The `layout` and `orientation` props can be used to change this behavior, allowing you to build horizontal and vertical stacks and grids. ### Horizontal stack# This example displays a horizontal list of cards. The left and right arrow keys can be used to navigate between the items. <ListBox aria-label="Albums" orientation="horizontal" items={albums} selectionMode="multiple"> {item => ( <ListBoxItem textValue={item.title}> <img src={item.image} alt="" /> <Text slot="label">{item.title}</Text> <Text slot="description">{item.artist}</Text> </ListBoxItem> )} </ListBox> <ListBox aria-label="Albums" orientation="horizontal" items={albums} selectionMode="multiple"> {item => ( <ListBoxItem textValue={item.title}> <img src={item.image} alt="" /> <Text slot="label">{item.title}</Text> <Text slot="description">{item.artist}</Text> </ListBoxItem> )} </ListBox> <ListBox aria-label="Albums" orientation="horizontal" items={albums} selectionMode="multiple" > {(item) => ( <ListBoxItem textValue={item .title} > <img src={item .image} alt="" /> <Text slot="label"> {item.title} </Text> <Text slot="description"> {item.artist} </Text> </ListBoxItem> )} </ListBox> Euphoric EchoesLuna Solstice Neon DreamscapeElectra Skyline Cosmic SerenadeOrion's Symphony Melancholy MelodiesViolet Mistral Rhythmic IllusionsMirage Beats Show CSS .react-aria-ListBox[data-orientation=horizontal], .react-aria-ListBox[data-layout=grid] { flex-direction: row; width: fit-content; max-width: 100%; padding: 4px; .react-aria-ListBoxItem { position: relative; margin: 0; padding: 4px; & img { object-fit: cover; aspect-ratio: 1/1; max-width: 150px; margin-bottom: 4px; border-radius: 4px; } &[data-selected] { background: none; color: inherit; &:after { content: '✓'; content: '✓' / ''; alt: ' '; position: absolute; top: 8px; right: 8px; background: var(--highlight-background); border: 2px solid var(--highlight-foreground); color: var(--highlight-foreground); width: 22px; height: 22px; border-radius: 22px; box-sizing: border-box; font-size: 14px; line-height: 1em; display: flex; align-items: center; justify-content: center; box-shadow: 0 0 8px rgb(0 0 0 / .5); } } &[data-focus-visible] { outline: 2px solid var(--focus-ring-color); outline-offset: -2px; } } } .react-aria-ListBox[data-orientation=horizontal], .react-aria-ListBox[data-layout=grid] { flex-direction: row; width: fit-content; max-width: 100%; padding: 4px; .react-aria-ListBoxItem { position: relative; margin: 0; padding: 4px; & img { object-fit: cover; aspect-ratio: 1/1; max-width: 150px; margin-bottom: 4px; border-radius: 4px; } &[data-selected] { background: none; color: inherit; &:after { content: '✓'; content: '✓' / ''; alt: ' '; position: absolute; top: 8px; right: 8px; background: var(--highlight-background); border: 2px solid var(--highlight-foreground); color: var(--highlight-foreground); width: 22px; height: 22px; border-radius: 22px; box-sizing: border-box; font-size: 14px; line-height: 1em; display: flex; align-items: center; justify-content: center; box-shadow: 0 0 8px rgb(0 0 0 / .5); } } &[data-focus-visible] { outline: 2px solid var(--focus-ring-color); outline-offset: -2px; } } } .react-aria-ListBox[data-orientation=horizontal], .react-aria-ListBox[data-layout=grid] { flex-direction: row; width: fit-content; max-width: 100%; padding: 4px; .react-aria-ListBoxItem { position: relative; margin: 0; padding: 4px; & img { object-fit: cover; aspect-ratio: 1/1; max-width: 150px; margin-bottom: 4px; border-radius: 4px; } &[data-selected] { background: none; color: inherit; &:after { content: '✓'; content: '✓' / ''; alt: ' '; position: absolute; top: 8px; right: 8px; background: var(--highlight-background); border: 2px solid var(--highlight-foreground); color: var(--highlight-foreground); width: 22px; height: 22px; border-radius: 22px; box-sizing: border-box; font-size: 14px; line-height: 1em; display: flex; align-items: center; justify-content: center; box-shadow: 0 0 8px rgb(0 0 0 / .5); } } &[data-focus-visible] { outline: 2px solid var(--focus-ring-color); outline-offset: -2px; } } } ### Vertical grid# The `layout` prop can be set to `"grid"` to enable two-dimensional keyboard navigation. By default, the grid scrolls vertically. The left, right, up, and down arrow keys can be used to navigate between the cards in this example. <ListBox aria-label="Albums" layout="grid" items={albums} selectionMode="multiple"> {item => ( <ListBoxItem textValue={item.title}> <img src={item.image} alt="" /> <Text slot="label">{item.title}</Text> <Text slot="description">{item.artist}</Text> </ListBoxItem> )} </ListBox> <ListBox aria-label="Albums" layout="grid" items={albums} selectionMode="multiple"> {item => ( <ListBoxItem textValue={item.title}> <img src={item.image} alt="" /> <Text slot="label">{item.title}</Text> <Text slot="description">{item.artist}</Text> </ListBoxItem> )} </ListBox> <ListBox aria-label="Albums" layout="grid" items={albums} selectionMode="multiple" > {(item) => ( <ListBoxItem textValue={item .title} > <img src={item .image} alt="" /> <Text slot="label"> {item.title} </Text> <Text slot="description"> {item.artist} </Text> </ListBoxItem> )} </ListBox> Euphoric EchoesLuna Solstice Neon DreamscapeElectra Skyline Cosmic SerenadeOrion's Symphony Melancholy MelodiesViolet Mistral Rhythmic IllusionsMirage Beats Show CSS .react-aria-ListBox[data-layout=grid] { display: grid; grid-template-columns: 1fr 1fr; scrollbar-gutter: stable; } .react-aria-ListBox[data-layout=grid] { display: grid; grid-template-columns: 1fr 1fr; scrollbar-gutter: stable; } .react-aria-ListBox[data-layout=grid] { display: grid; grid-template-columns: 1fr 1fr; scrollbar-gutter: stable; } ### Horizontal grid# The `layout="grid"` and `orientation="horizontal"` props can be combined to build a two dimensional grid where the items are grouped into columns, and the grid scrolls horizontally. <ListBox aria-label="Albums" layout="grid" orientation="horizontal" items={albums} selectionMode="multiple"> {item => ( <ListBoxItem textValue={item.title}> <img src={item.image} alt="" /> <Text slot="label">{item.title}</Text> <Text slot="description">{item.artist}</Text> </ListBoxItem> )} </ListBox> <ListBox aria-label="Albums" layout="grid" orientation="horizontal" items={albums} selectionMode="multiple"> {item => ( <ListBoxItem textValue={item.title}> <img src={item.image} alt="" /> <Text slot="label">{item.title}</Text> <Text slot="description">{item.artist}</Text> </ListBoxItem> )} </ListBox> <ListBox aria-label="Albums" layout="grid" orientation="horizontal" items={albums} selectionMode="multiple" > {(item) => ( <ListBoxItem textValue={item .title} > <img src={item .image} alt="" /> <Text slot="label"> {item.title} </Text> <Text slot="description"> {item.artist} </Text> </ListBoxItem> )} </ListBox> Euphoric EchoesLuna Solstice Neon DreamscapeElectra Skyline Cosmic SerenadeOrion's Symphony Melancholy MelodiesViolet Mistral Rhythmic IllusionsMirage Beats Show CSS .react-aria-ListBox[data-layout=grid][data-orientation=horizontal] { width: 100%; max-width: none; display: grid; grid-auto-flow: column; grid-template-rows: 58px 58px; grid-template-columns: none; grid-auto-columns: 250px; max-height: 200px; gap: 8px; .react-aria-ListBoxItem { display: grid; grid-template-areas: "image ." "image title" "image description" "image ."; grid-template-columns: auto 1fr; grid-template-rows: 1fr auto auto 1fr; column-gap: 8px; & img { width: 50px; height: 50px; grid-area: image; margin-bottom: 0; } [slot=label] { grid-area: title; } [slot=description] { grid-area: description; } } } .react-aria-ListBox[data-layout=grid][data-orientation=horizontal] { width: 100%; max-width: none; display: grid; grid-auto-flow: column; grid-template-rows: 58px 58px; grid-template-columns: none; grid-auto-columns: 250px; max-height: 200px; gap: 8px; .react-aria-ListBoxItem { display: grid; grid-template-areas: "image ." "image title" "image description" "image ."; grid-template-columns: auto 1fr; grid-template-rows: 1fr auto auto 1fr; column-gap: 8px; & img { width: 50px; height: 50px; grid-area: image; margin-bottom: 0; } [slot=label] { grid-area: title; } [slot=description] { grid-area: description; } } } .react-aria-ListBox[data-layout=grid][data-orientation=horizontal] { width: 100%; max-width: none; display: grid; grid-auto-flow: column; grid-template-rows: 58px 58px; grid-template-columns: none; grid-auto-columns: 250px; max-height: 200px; gap: 8px; .react-aria-ListBoxItem { display: grid; grid-template-areas: "image ." "image title" "image description" "image ."; grid-template-columns: auto 1fr; grid-template-rows: 1fr auto auto 1fr; column-gap: 8px; & img { width: 50px; height: 50px; grid-area: image; margin-bottom: 0; } [slot=label] { grid-area: title; } [slot=description] { grid-area: description; } } } ## Asynchronous loading# * * * This example uses the useAsyncList hook to handle asynchronous loading of data from a server. You may additionally want to display a spinner to indicate the loading state to the user, or support features like infinite scroll to load more data. import {useAsyncList} from 'react-stately'; interface Character { name: string; } function AsyncLoadingExample() { let list = useAsyncList<Character>({ async load({ signal, filterText }) { let res = await fetch( `https://pokeapi.co/api/v2/pokemon`, { signal } ); let json = await res.json(); return { items: json.results }; } }); return ( <ListBox aria-label="Pick a Pokemon" items={list.items} selectionMode="single" > {(item) => <ListBoxItem id={item.name}>{item.name}</ListBoxItem>} </ListBox> ); } import {useAsyncList} from 'react-stately'; interface Character { name: string; } function AsyncLoadingExample() { let list = useAsyncList<Character>({ async load({ signal, filterText }) { let res = await fetch( `https://pokeapi.co/api/v2/pokemon`, { signal } ); let json = await res.json(); return { items: json.results }; } }); return ( <ListBox aria-label="Pick a Pokemon" items={list.items} selectionMode="single" > {(item) => ( <ListBoxItem id={item.name}> {item.name} </ListBoxItem> )} </ListBox> ); } import {useAsyncList} from 'react-stately'; interface Character { name: string; } function AsyncLoadingExample() { let list = useAsyncList< Character >({ async load( { signal, filterText } ) { let res = await fetch( `https://pokeapi.co/api/v2/pokemon`, { signal } ); let json = await res .json(); return { items: json.results }; } }); return ( <ListBox aria-label="Pick a Pokemon" items={list.items} selectionMode="single" > {(item) => ( <ListBoxItem id={item.name} > {item.name} </ListBoxItem> )} </ListBox> ); } bulbasaur ivysaur venusaur charmander charmeleon charizard squirtle wartortle blastoise caterpie metapod butterfree weedle kakuna beedrill pidgey pidgeotto pidgeot rattata raticate ## Disabled items# * * * A `ListBoxItem` can be disabled with the `isDisabled` prop. Disabled items are not focusable, selectable, or keyboard navigable. <ListBox aria-label="Choose sandwich contents with disabled items" selectionMode="multiple"> <ListBoxItem>Lettuce</ListBoxItem> <ListBoxItem>Tomato</ListBoxItem> <ListBoxItem>Cheese</ListBoxItem> <ListBoxItem isDisabled>Tuna Salad</ListBoxItem> <ListBoxItem>Egg Salad</ListBoxItem> <ListBoxItem>Ham</ListBoxItem> </ListBox> <ListBox aria-label="Choose sandwich contents with disabled items" selectionMode="multiple"> <ListBoxItem>Lettuce</ListBoxItem> <ListBoxItem>Tomato</ListBoxItem> <ListBoxItem>Cheese</ListBoxItem> <ListBoxItem isDisabled>Tuna Salad</ListBoxItem> <ListBoxItem>Egg Salad</ListBoxItem> <ListBoxItem>Ham</ListBoxItem> </ListBox> <ListBox aria-label="Choose sandwich contents with disabled items" selectionMode="multiple" > <ListBoxItem> Lettuce </ListBoxItem> <ListBoxItem> Tomato </ListBoxItem> <ListBoxItem> Cheese </ListBoxItem> <ListBoxItem isDisabled > Tuna Salad </ListBoxItem> <ListBoxItem> Egg Salad </ListBoxItem> <ListBoxItem> Ham </ListBoxItem> </ListBox> Lettuce Tomato Cheese Tuna Salad Egg Salad Ham Show CSS .react-aria-ListBoxItem { &[data-disabled] { color: var(--text-color-disabled); } } .react-aria-ListBoxItem { &[data-disabled] { color: var(--text-color-disabled); } } .react-aria-ListBoxItem { &[data-disabled] { color: var(--text-color-disabled); } } In dynamic collections, it may be more convenient to use the `disabledKeys` prop at the `ListBox` level instead of `isDisabled` on individual items. Each key in this list corresponds with the `id` prop passed to the `ListBoxItem` component, or automatically derived from the values passed to the `items` prop (see the Collections for more details). An item is considered disabled if its id exists in `disabledKeys` or if it has `isDisabled`. function Example() { let options = [ { id: 1, name: 'Aardvark' }, { id: 2, name: 'Cat' }, { id: 3, name: 'Dog' }, { id: 4, name: 'Kangaroo' }, { id: 5, name: 'Koala' }, { id: 6, name: 'Penguin' }, { id: 7, name: 'Snake' }, { id: 8, name: 'Turtle' }, { id: 9, name: 'Wombat' } ]; return ( <ListBox aria-label="Animals with disabledKeys" items={options} selectionMode="single" disabledKeys={[4, 6]} > {(item) => <ListBoxItem>{item.name}</ListBoxItem>} </ListBox> ); } function Example() { let options = [ { id: 1, name: 'Aardvark' }, { id: 2, name: 'Cat' }, { id: 3, name: 'Dog' }, { id: 4, name: 'Kangaroo' }, { id: 5, name: 'Koala' }, { id: 6, name: 'Penguin' }, { id: 7, name: 'Snake' }, { id: 8, name: 'Turtle' }, { id: 9, name: 'Wombat' } ]; return ( <ListBox aria-label="Animals with disabledKeys" items={options} selectionMode="single" disabledKeys={[4, 6]} > {(item) => <ListBoxItem>{item.name}</ListBoxItem>} </ListBox> ); } function Example() { let options = [ { id: 1, name: 'Aardvark' }, { id: 2, name: 'Cat' }, { id: 3, name: 'Dog' }, { id: 4, name: 'Kangaroo' }, { id: 5, name: 'Koala' }, { id: 6, name: 'Penguin' }, { id: 7, name: 'Snake' }, { id: 8, name: 'Turtle' }, { id: 9, name: 'Wombat' } ]; return ( <ListBox aria-label="Animals with disabledKeys" items={options} selectionMode="single" disabledKeys={[ 4, 6 ]} > {(item) => ( <ListBoxItem> {item.name} </ListBoxItem> )} </ListBox> ); } Aardvark Cat Dog Kangaroo Koala Penguin Snake Turtle Wombat ## Empty state# * * * Use the `renderEmptyState` prop to customize what the `ListBox` will display if there are no items. <ListBox aria-label="Search results" renderEmptyState={() => 'No results found.'}> {[]} </ListBox> <ListBox aria-label="Search results" renderEmptyState={() => 'No results found.'}> {[]} </ListBox> <ListBox aria-label="Search results" renderEmptyState={() => 'No results found.'}> {[]} </ListBox> No results found. Show CSS .react-aria-ListBox { &[data-empty] { align-items: center; justify-content: center; font-style: italic; } } .react-aria-ListBox { &[data-empty] { align-items: center; justify-content: center; font-style: italic; } } .react-aria-ListBox { &[data-empty] { align-items: center; justify-content: center; font-style: italic; } } ## Drag and drop# * * * ListBox supports drag and drop interactions when the `dragAndDropHooks` prop is provided using the `useDragAndDrop` hook. Users can drop data on the list as a whole, on individual items, insert new items between existing ones, or reorder items. React Aria supports traditional mouse and touch based drag and drop, but also implements keyboard and screen reader friendly interactions. Users can press Enter on a draggable element to enter drag and drop mode. Then, they can press Tab to navigate between drop targets. A droppable collection is treated as a single drop target, so that users can easily tab past it to get to the next drop target. Within a droppable collection, keys such as ArrowDown and ArrowUp can be used to select a _drop position_, such as on an item, or between items. See the drag and drop introduction to learn more. ### Reorderable# This example shows a basic list that allows users to reorder items via drag and drop. This is enabled using the `onReorder` event handler, provided to the `useDragAndDrop` hook. The `getItems` function must also be implemented for items to become draggable. See below for more details. This uses useListData from React Stately to manage the item list. Note that `useListData` is a convenience hook, not a requirement. You can manage your state however you wish. import {useListData} from 'react-stately'; import {useDragAndDrop} from 'react-aria-components'; function Example() { let list = useListData({ initialItems: [ { id: 1, name: 'Adobe Photoshop' }, { id: 2, name: 'Adobe XD' }, { id: 3, name: 'Adobe Dreamweaver' }, { id: 4, name: 'Adobe InDesign' }, { id: 5, name: 'Adobe Connect' } ] }); let { dragAndDropHooks } = useDragAndDrop({ getItems: (keys) => [...keys].map((key) => ({ 'text/plain': list.getItem(key).name })), onReorder(e) { if (e.target.dropPosition === 'before') { list.moveBefore(e.target.key, e.keys); } else if (e.target.dropPosition === 'after') { list.moveAfter(e.target.key, e.keys); } } }); return ( <ListBox aria-label="Reorderable list" selectionMode="multiple" items={list.items} dragAndDropHooks={dragAndDropHooks} > {(item) => <ListBoxItem>{item.name}</ListBoxItem>} </ListBox> ); } import {useListData} from 'react-stately'; import {useDragAndDrop} from 'react-aria-components'; function Example() { let list = useListData({ initialItems: [ { id: 1, name: 'Adobe Photoshop' }, { id: 2, name: 'Adobe XD' }, { id: 3, name: 'Adobe Dreamweaver' }, { id: 4, name: 'Adobe InDesign' }, { id: 5, name: 'Adobe Connect' } ] }); let { dragAndDropHooks } = useDragAndDrop({ getItems: (keys) => [...keys].map((key) => ({ 'text/plain': list.getItem(key).name })), onReorder(e) { if (e.target.dropPosition === 'before') { list.moveBefore(e.target.key, e.keys); } else if (e.target.dropPosition === 'after') { list.moveAfter(e.target.key, e.keys); } } }); return ( <ListBox aria-label="Reorderable list" selectionMode="multiple" items={list.items} dragAndDropHooks={dragAndDropHooks} > {(item) => <ListBoxItem>{item.name}</ListBoxItem>} </ListBox> ); } import {useListData} from 'react-stately'; import {useDragAndDrop} from 'react-aria-components'; function Example() { let list = useListData( { initialItems: [ { id: 1, name: 'Adobe Photoshop' }, { id: 2, name: 'Adobe XD' }, { id: 3, name: 'Adobe Dreamweaver' }, { id: 4, name: 'Adobe InDesign' }, { id: 5, name: 'Adobe Connect' } ] } ); let { dragAndDropHooks } = useDragAndDrop({ getItems: (keys) => [...keys].map( (key) => ({ 'text/plain': list.getItem( key ).name }) ), onReorder(e) { if ( e.target .dropPosition === 'before' ) { list.moveBefore( e.target.key, e.keys ); } else if ( e.target .dropPosition === 'after' ) { list.moveAfter( e.target.key, e.keys ); } } }); return ( <ListBox aria-label="Reorderable list" selectionMode="multiple" items={list.items} dragAndDropHooks={dragAndDropHooks} > {(item) => ( <ListBoxItem> {item.name} </ListBoxItem> )} </ListBox> ); } Adobe Photoshop Adobe XD Adobe Dreamweaver Adobe InDesign Adobe Connect Show CSS .react-aria-ListBoxItem { &[data-dragging] { opacity: 0.6; } } .react-aria-DropIndicator[data-drop-target] { outline: 1px solid var(--highlight-background); } .react-aria-ListBoxItem { &[data-dragging] { opacity: 0.6; } } .react-aria-DropIndicator[data-drop-target] { outline: 1px solid var(--highlight-background); } .react-aria-ListBoxItem { &[data-dragging] { opacity: 0.6; } } .react-aria-DropIndicator[data-drop-target] { outline: 1px solid var(--highlight-background); } ### Custom drag preview# By default, the drag preview shown under the user's pointer or finger is a copy of the original element that started the drag. A custom preview can be rendered by implementing the `renderDragPreview` function, passed to `useDragAndDrop`. This receives the dragged data that was returned by `getItems`, and returns a rendered preview for those items. This example renders a custom drag preview which shows the number of items being dragged. import {useListData} from 'react-stately'; import {useDragAndDrop} from 'react-aria-components'; function Example() { let {dragAndDropHooks} = useDragAndDrop({ // ... renderDragPreview(items) { return ( <div className="drag-preview"> {items[0]['text/plain']} <span className="badge">{items.length}</span> </div> ); } }); // ... } import {useListData} from 'react-stately'; import {useDragAndDrop} from 'react-aria-components'; function Example() { let {dragAndDropHooks} = useDragAndDrop({ // ... renderDragPreview(items) { return ( <div className="drag-preview"> {items[0]['text/plain']} <span className="badge">{items.length}</span> </div> ); } }); // ... } import {useListData} from 'react-stately'; import {useDragAndDrop} from 'react-aria-components'; function Example() { let { dragAndDropHooks } = useDragAndDrop({ // ... renderDragPreview( items ) { return ( <div className="drag-preview"> {items[0][ 'text/plain' ]} <span className="badge"> {items .length} </span> </div> ); } }); // ... } Adobe Photoshop Adobe XD Adobe Dreamweaver Adobe InDesign Adobe Connect Show CSS .drag-preview { width: 150px; padding: 4px 8px; display: flex; align-items: center; justify-content: space-between; gap: 4px; background: var(--highlight-background); color: var(--highlight-foreground); border-radius: 4px; .badge { background: var(--highlight-foreground); color: var(--highlight-background); padding: 0 8px; border-radius: 4px; } } .drag-preview { width: 150px; padding: 4px 8px; display: flex; align-items: center; justify-content: space-between; gap: 4px; background: var(--highlight-background); color: var(--highlight-foreground); border-radius: 4px; .badge { background: var(--highlight-foreground); color: var(--highlight-background); padding: 0 8px; border-radius: 4px; } } .drag-preview { width: 150px; padding: 4px 8px; display: flex; align-items: center; justify-content: space-between; gap: 4px; background: var(--highlight-background); color: var(--highlight-foreground); border-radius: 4px; .badge { background: var(--highlight-foreground); color: var(--highlight-background); padding: 0 8px; border-radius: 4px; } } ### Drag data# Data for draggable items can be provided in multiple formats at once. This allows drop targets to choose data in a format that they understand. For example, you could serialize a complex object as JSON in a custom format for use within your own application, and also provide plain text and/or rich HTML fallbacks that can be used when a user drops data in an external application (e.g. an email message). This can be done by returning multiple keys for an item from the `getItems` function. Types can either be a standard mime type for interoperability with external applications, or a custom string for use within your own app. This example provides representations of each item as plain text, HTML, and a custom app-specific data format. Dropping on the drop targets in this page will use the custom data format to render formatted items. If you drop in an external application supporting rich text, the HTML representation will be used. Dropping in a text editor will use the plain text format. function DraggableListBox() { let items = new Map([ ['ps', { name: 'Photoshop', style: 'strong' }], ['xd', { name: 'XD', style: 'strong' }], ['id', { name: 'InDesign', style: 'strong' }], ['dw', { name: 'Dreamweaver', style: 'em' }], ['co', { name: 'Connect', style: 'em' }] ]); let { dragAndDropHooks } = useDragAndDrop({ getItems(keys) { return [...keys].map((key) => { let item = items.get(key as string)!; return { 'text/plain': item.name, 'text/html': `<${item.style}>${item.name}</${item.style}>`, 'custom-app-type': JSON.stringify({ id: key, ...item }) }; }); } }); return ( <ListBox aria-label="Draggable list" selectionMode="multiple" items={items} dragAndDropHooks={dragAndDropHooks} > {([id, item]) => ( <ListBoxItem id={id} textValue={item.name}> {React.createElement(item.style || 'span', null, item.name)} </ListBoxItem> )} </ListBox> ); } <div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }}> <DraggableListBox /> {/* see below */} <DroppableListBox /> </div> function DraggableListBox() { let items = new Map([ ['ps', { name: 'Photoshop', style: 'strong' }], ['xd', { name: 'XD', style: 'strong' }], ['id', { name: 'InDesign', style: 'strong' }], ['dw', { name: 'Dreamweaver', style: 'em' }], ['co', { name: 'Connect', style: 'em' }] ]); let { dragAndDropHooks } = useDragAndDrop({ getItems(keys) { return [...keys].map((key) => { let item = items.get(key as string)!; return { 'text/plain': item.name, 'text/html': `<${item.style}>${item.name}</${item.style}>`, 'custom-app-type': JSON.stringify({ id: key, ...item }) }; }); } }); return ( <ListBox aria-label="Draggable list" selectionMode="multiple" items={items} dragAndDropHooks={dragAndDropHooks} > {([id, item]) => ( <ListBoxItem id={id} textValue={item.name}> {React.createElement( item.style || 'span', null, item.name )} </ListBoxItem> )} </ListBox> ); } <div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }} > <DraggableListBox /> {/* see below */} <DroppableListBox /> </div> function DraggableListBox() { let items = new Map([ ['ps', { name: 'Photoshop', style: 'strong' }], ['xd', { name: 'XD', style: 'strong' }], ['id', { name: 'InDesign', style: 'strong' }], ['dw', { name: 'Dreamweaver', style: 'em' }], ['co', { name: 'Connect', style: 'em' }] ]); let { dragAndDropHooks } = useDragAndDrop({ getItems(keys) { return [...keys] .map((key) => { let item = items.get( key as string )!; return { 'text/plain': item.name, 'text/html': `<${item.style}>${item.name}</${item.style}>`, 'custom-app-type': JSON .stringify( { id: key, ...item } ) }; }); } }); return ( <ListBox aria-label="Draggable list" selectionMode="multiple" items={items} dragAndDropHooks={dragAndDropHooks} > {([id, item]) => ( <ListBoxItem id={id} textValue={item .name} > {React .createElement( item .style || 'span', null, item.name )} </ListBoxItem> )} </ListBox> ); } <div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }} > <DraggableListBox /> {/* see below */} <DroppableListBox /> </div> **Photoshop** **XD** **InDesign** _Dreamweaver_ _Connect_ Drop items here ### Dropping on the collection# Dropping on the ListBox as a whole can be enabled using the `onRootDrop` event. When a valid drag hovers over the ListBox, it receives the `isDropTarget` state and can be styled using the `[data-drop-target]` CSS selector. interface Item { id: number; name: string; } function Example() { let [items, setItems] = React.useState<Item[]>([]); let { dragAndDropHooks } = useDragAndDrop({ async onRootDrop(e) { let items = await Promise.all(e.items.map(async (item, i) => { let name = item.kind === 'text' ? await item.getText('text/plain') : item.name; return { id: i, name }; })); setItems(items); } }); return ( <div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }}> <DraggableListBox /> <ListBox aria-label="Droppable list" items={items} dragAndDropHooks={dragAndDropHooks} renderEmptyState={() => 'Drop items here'} > {(item) => <ListBoxItem>{item.name}</ListBoxItem>} </ListBox> </div> ); } interface Item { id: number; name: string; } function Example() { let [items, setItems] = React.useState<Item[]>([]); let { dragAndDropHooks } = useDragAndDrop({ async onRootDrop(e) { let items = await Promise.all( e.items.map(async (item, i) => { let name = item.kind === 'text' ? await item.getText('text/plain') : item.name; return { id: i, name }; }) ); setItems(items); } }); return ( <div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }} > <DraggableListBox /> <ListBox aria-label="Droppable list" items={items} dragAndDropHooks={dragAndDropHooks} renderEmptyState={() => 'Drop items here'} > {(item) => <ListBoxItem>{item.name}</ListBoxItem>} </ListBox> </div> ); } interface Item { id: number; name: string; } function Example() { let [items, setItems] = React.useState< Item[] >([]); let { dragAndDropHooks } = useDragAndDrop({ async onRootDrop(e) { let items = await Promise .all( e.items.map( async ( item, i ) => { let name = item .kind === 'text' ? await item .getText( 'text/plain' ) : item .name; return { id: i, name }; } ) ); setItems(items); } }); return ( <div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }} > <DraggableListBox /> <ListBox aria-label="Droppable list" items={items} dragAndDropHooks={dragAndDropHooks} renderEmptyState={() => 'Drop items here'} > {(item) => ( <ListBoxItem> {item.name} </ListBoxItem> )} </ListBox> </div> ); } **Photoshop** **XD** **InDesign** _Dreamweaver_ _Connect_ Drop items here Show CSS .react-aria-ListBox[data-drop-target] { outline: 2px solid var(--highlight-background); outline-offset: -1px; background: var(--highlight-overlay) } .react-aria-ListBox[data-drop-target] { outline: 2px solid var(--highlight-background); outline-offset: -1px; background: var(--highlight-overlay) } .react-aria-ListBox[data-drop-target] { outline: 2px solid var(--highlight-background); outline-offset: -1px; background: var(--highlight-overlay) } ### Dropping on items# Dropping on items can be enabled using the `onItemDrop` event. When a valid drag hovers over an item, it receives the `isDropTarget` state and can be styled using the `[data-drop-target]` CSS selector. function Example() { let { dragAndDropHooks } = useDragAndDrop({ onItemDrop(e) { alert(`Dropped on ${e.target.key}`); } }); return ( <div style={{display: 'flex', gap: 12, flexWrap: 'wrap'}}> {/* see above */} <DraggableListBox /> <ListBox aria-label="Droppable list" dragAndDropHooks={dragAndDropHooks}> <ListBoxItem id="applications">Applications</ListBoxItem> <ListBoxItem id="documents">Documents</ListBoxItem> <ListBoxItem id="pictures">Pictures</ListBoxItem> </ListBox> </div> ); } function Example() { let { dragAndDropHooks } = useDragAndDrop({ onItemDrop(e) { alert(`Dropped on ${e.target.key}`); } }); return ( <div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }} > {/* see above */} <DraggableListBox /> <ListBox aria-label="Droppable list" dragAndDropHooks={dragAndDropHooks} > <ListBoxItem id="applications"> Applications </ListBoxItem> <ListBoxItem id="documents">Documents</ListBoxItem> <ListBoxItem id="pictures">Pictures</ListBoxItem> </ListBox> </div> ); } function Example() { let { dragAndDropHooks } = useDragAndDrop({ onItemDrop(e) { alert( `Dropped on ${e.target.key}` ); } }); return ( <div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }} > {/* see above */} <DraggableListBox /> <ListBox aria-label="Droppable list" dragAndDropHooks={dragAndDropHooks} > <ListBoxItem id="applications"> Applications </ListBoxItem> <ListBoxItem id="documents"> Documents </ListBoxItem> <ListBoxItem id="pictures"> Pictures </ListBoxItem> </ListBox> </div> ); } **Photoshop** **XD** **InDesign** _Dreamweaver_ _Connect_ Applications Documents Pictures Show CSS .react-aria-ListBoxItem[data-drop-target] { outline: 2px solid var(--highlight-background); background: var(--highlight-overlay) } .react-aria-ListBoxItem[data-drop-target] { outline: 2px solid var(--highlight-background); background: var(--highlight-overlay) } .react-aria-ListBoxItem[data-drop-target] { outline: 2px solid var(--highlight-background); background: var(--highlight-overlay) } ### Dropping between items# Dropping between items can be enabled using the `onInsert` event. ListBox renders a `DropIndicator` between items to indicate the insertion position, which can be styled using the `.react-aria-DropIndicator` selector. When it is active, it receives the `[data-drop-target]` state. function Example() { let list = useListData({ initialItems: [ { id: 1, name: 'Illustrator' }, { id: 2, name: 'Premiere' }, { id: 3, name: 'Acrobat' } ] }); let { dragAndDropHooks } = useDragAndDrop({ async onInsert(e) { let items = await Promise.all(e.items.map(async (item) => { let name = item.kind === 'text' ? await item.getText('text/plain') : item.name; return { id: Math.random(), name }; })); if (e.target.dropPosition === 'before') { list.insertBefore(e.target.key, ...items); } else if (e.target.dropPosition === 'after') { list.insertAfter(e.target.key, ...items); } } }); return ( <div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }}> <DraggableListBox /> <ListBox aria-label="Droppable list" items={list.items} dragAndDropHooks={dragAndDropHooks} > {(item) => <ListBoxItem>{item.name}</ListBoxItem>} </ListBox> </div> ); } function Example() { let list = useListData({ initialItems: [ { id: 1, name: 'Illustrator' }, { id: 2, name: 'Premiere' }, { id: 3, name: 'Acrobat' } ] }); let { dragAndDropHooks } = useDragAndDrop({ async onInsert(e) { let items = await Promise.all( e.items.map(async (item) => { let name = item.kind === 'text' ? await item.getText('text/plain') : item.name; return { id: Math.random(), name }; }) ); if (e.target.dropPosition === 'before') { list.insertBefore(e.target.key, ...items); } else if (e.target.dropPosition === 'after') { list.insertAfter(e.target.key, ...items); } } }); return ( <div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }} > <DraggableListBox /> <ListBox aria-label="Droppable list" items={list.items} dragAndDropHooks={dragAndDropHooks} > {(item) => <ListBoxItem>{item.name}</ListBoxItem>} </ListBox> </div> ); } function Example() { let list = useListData( { initialItems: [ { id: 1, name: 'Illustrator' }, { id: 2, name: 'Premiere' }, { id: 3, name: 'Acrobat' } ] } ); let { dragAndDropHooks } = useDragAndDrop({ async onInsert(e) { let items = await Promise .all( e.items.map( async (item) => { let name = item .kind === 'text' ? await item .getText( 'text/plain' ) : item .name; return { id: Math .random(), name }; } ) ); if ( e.target .dropPosition === 'before' ) { list .insertBefore( e.target.key, ...items ); } else if ( e.target .dropPosition === 'after' ) { list.insertAfter( e.target.key, ...items ); } } }); return ( <div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }} > <DraggableListBox /> <ListBox aria-label="Droppable list" items={list .items} dragAndDropHooks={dragAndDropHooks} > {(item) => ( <ListBoxItem> {item.name} </ListBoxItem> )} </ListBox> </div> ); } **Photoshop** **XD** **InDesign** _Dreamweaver_ _Connect_ Illustrator Premiere Acrobat Show CSS .react-aria-DropIndicator[data-drop-target] { outline: 1px solid var(--highlight-background); } .react-aria-DropIndicator[data-drop-target] { outline: 1px solid var(--highlight-background); } .react-aria-DropIndicator[data-drop-target] { outline: 1px solid var(--highlight-background); } A custom drop indicator can also be rendered with the `renderDropIndicator` function. This lets you customize the DOM structure and CSS classes applied to the drop indicator. import {DropIndicator} from 'react-aria-components'; function Example() { let { dragAndDropHooks } = useDragAndDrop({ // ... renderDropIndicator(target) { return ( <DropIndicator target={target} className={({ isDropTarget }) => `my-drop-indicator ${isDropTarget ? 'active' : ''}`} /> ); } }); // ... } import {DropIndicator} from 'react-aria-components'; function Example() { let { dragAndDropHooks } = useDragAndDrop({ // ... renderDropIndicator(target) { return ( <DropIndicator target={target} className={({ isDropTarget }) => `my-drop-indicator ${ isDropTarget ? 'active' : '' }`} /> ); } }); // ... } import {DropIndicator} from 'react-aria-components'; function Example() { let { dragAndDropHooks } = useDragAndDrop({ // ... renderDropIndicator( target ) { return ( <DropIndicator target={target} className={( { isDropTarget } ) => `my-drop-indicator ${ isDropTarget ? 'active' : '' }`} /> ); } }); // ... } **Photoshop** **XD** **InDesign** _Dreamweaver_ _Connect_ Illustrator Premiere Acrobat Show CSS .my-drop-indicator.active { outline: 1px solid #e70073; } .my-drop-indicator.active { outline: 1px solid #e70073; } .my-drop-indicator.active { outline: 1px solid #e70073; } ### Drop data# `ListBox` allows users to drop one or more **drag items**, each of which contains data to be transferred from the drag source to drop target. There are three kinds of drag items: * `text` – represents data inline as a string in one or more formats * `file` – references a file on the user's device * `directory` – references the contents of a directory #### Text# A `TextDropItem` represents textual data in one or more different formats. These may be either standard mime types or custom app-specific formats. Representing data in multiple formats allows drop targets both within and outside an application to choose data in a format that they understand. For example, a complex object may be serialized in a custom format for use within an application, with fallbacks in plain text and/or rich HTML that can be used when a user drops data from an external application. The example below uses the `acceptedDragTypes` prop to accept items that include a custom app-specific type, which is retrieved using the item's `getText` method. The same draggable component as used in the above example is used here, but rather than displaying the plain text representation, the custom format is used instead. When `acceptedDragTypes` is specified, the dropped items are filtered to include only items that include the accepted types. import {isTextDropItem} from 'react-aria-components'; interface TextItem { id: string; name: string; style: string; } function DroppableListBox() { let [items, setItems] = React.useState<TextItem[]>([]); let { dragAndDropHooks } = useDragAndDrop({ acceptedDragTypes: ['custom-app-type'], async onRootDrop(e) { let items = await Promise.all( e.items .filter(isTextDropItem) .map(async (item) => JSON.parse(await item.getText('custom-app-type')) ) ); setItems(items); } }); return ( <ListBox aria-label="Droppable list" items={items} dragAndDropHooks={dragAndDropHooks} renderEmptyState={() => 'Drop items here'} > {(item) => ( <ListBoxItem textValue={item.name}> {React.createElement(item.style || 'span', null, item.name)} </ListBoxItem> )} </ListBox> ); } <div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }}> {/* see above */} <DraggableListBox /> <DroppableListBox /> </div> import {isTextDropItem} from 'react-aria-components'; interface TextItem { id: string; name: string; style: string; } function DroppableListBox() { let [items, setItems] = React.useState<TextItem[]>([]); let { dragAndDropHooks } = useDragAndDrop({ acceptedDragTypes: ['custom-app-type'], async onRootDrop(e) { let items = await Promise.all( e.items .filter(isTextDropItem) .map(async (item) => JSON.parse( await item.getText('custom-app-type') ) ) ); setItems(items); } }); return ( <ListBox aria-label="Droppable list" items={items} dragAndDropHooks={dragAndDropHooks} renderEmptyState={() => 'Drop items here'} > {(item) => ( <ListBoxItem textValue={item.name}> {React.createElement( item.style || 'span', null, item.name )} </ListBoxItem> )} </ListBox> ); } <div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }} > {/* see above */} <DraggableListBox /> <DroppableListBox /> </div> import {isTextDropItem} from 'react-aria-components'; interface TextItem { id: string; name: string; style: string; } function DroppableListBox() { let [items, setItems] = React.useState< TextItem[] >([]); let { dragAndDropHooks } = useDragAndDrop({ acceptedDragTypes: [ 'custom-app-type' ], async onRootDrop(e) { let items = await Promise .all( e.items .filter( isTextDropItem ) .map( async (item) => JSON .parse( await item .getText( 'custom-app-type' ) ) ) ); setItems(items); } }); return ( <ListBox aria-label="Droppable list" items={items} dragAndDropHooks={dragAndDropHooks} renderEmptyState={() => 'Drop items here'} > {(item) => ( <ListBoxItem textValue={item .name} > {React .createElement( item .style || 'span', null, item.name )} </ListBoxItem> )} </ListBox> ); } <div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }} > {/* see above */} <DraggableListBox /> <DroppableListBox /> </div> **Photoshop** **XD** **InDesign** _Dreamweaver_ _Connect_ Drop items here #### Files# A `FileDropItem` references a file on the user's device. It includes the name and mime type of the file, and methods to read the contents as plain text, or retrieve a native File object which can be attached to form data for uploading. This example accepts JPEG and PNG image files, and renders them by creating a local object URL. When the list is empty, you can drop on the whole collection, and otherwise items can be inserted. import {isFileDropItem} from 'react-aria-components'; interface ImageItem { id: number; url: string; name: string; } function Example() { let [items, setItems] = React.useState<ImageItem[]>([]); let { dragAndDropHooks } = useDragAndDrop({ acceptedDragTypes: ['image/jpeg', 'image/png'], async onRootDrop(e) { let items = await Promise.all( e.items.filter(isFileDropItem).map(async (item) => ({ id: Math.random(), url: URL.createObjectURL(await item.getFile()), name: item.name })) ); setItems(items); } }); return ( <ListBox aria-label="Droppable list" items={items} dragAndDropHooks={dragAndDropHooks} renderEmptyState={() => 'Drop items here'} > {(item) => ( <ListBoxItem textValue={item.name}> <div className="image-item"> <img src={item.url} /> <span>{item.name}</span> </div> </ListBoxItem> )} </ListBox> ); } import {isFileDropItem} from 'react-aria-components'; interface ImageItem { id: number; url: string; name: string; } function Example() { let [items, setItems] = React.useState<ImageItem[]>([]); let { dragAndDropHooks } = useDragAndDrop({ acceptedDragTypes: ['image/jpeg', 'image/png'], async onRootDrop(e) { let items = await Promise.all( e.items.filter(isFileDropItem).map( async (item) => ({ id: Math.random(), url: URL.createObjectURL(await item.getFile()), name: item.name }) ) ); setItems(items); } }); return ( <ListBox aria-label="Droppable list" items={items} dragAndDropHooks={dragAndDropHooks} renderEmptyState={() => 'Drop items here'} > {(item) => ( <ListBoxItem textValue={item.name}> <div className="image-item"> <img src={item.url} /> <span>{item.name}</span> </div> </ListBoxItem> )} </ListBox> ); } import {isFileDropItem} from 'react-aria-components'; interface ImageItem { id: number; url: string; name: string; } function Example() { let [items, setItems] = React.useState< ImageItem[] >([]); let { dragAndDropHooks } = useDragAndDrop({ acceptedDragTypes: [ 'image/jpeg', 'image/png' ], async onRootDrop(e) { let items = await Promise .all( e.items .filter( isFileDropItem ).map( async (item) => ({ id: Math .random(), url: URL .createObjectURL( await item .getFile() ), name: item .name }) ) ); setItems(items); } }); return ( <ListBox aria-label="Droppable list" items={items} dragAndDropHooks={dragAndDropHooks} renderEmptyState={() => 'Drop items here'} > {(item) => ( <ListBoxItem textValue={item .name} > <div className="image-item"> <img src={item .url} /> <span> {item.name} </span> </div> </ListBoxItem> )} </ListBox> ); } Drop items here Show CSS .image-item { display: flex; height: 50px; gap: 10px; align-items: center; } .image-item img { height: 100%; aspect-ratio: 1/1; object-fit: contain; } .image-item span { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .image-item { display: flex; height: 50px; gap: 10px; align-items: center; } .image-item img { height: 100%; aspect-ratio: 1/1; object-fit: contain; } .image-item span { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .image-item { display: flex; height: 50px; gap: 10px; align-items: center; } .image-item img { height: 100%; aspect-ratio: 1/1; object-fit: contain; } .image-item span { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } #### Directories# A `DirectoryDropItem` references the contents of a directory on the user's device. It includes the name of the directory, as well as a method to iterate through the files and folders within the directory. The contents of any folders within the directory can be accessed recursively. The `getEntries` method returns an async iterable object, which can be used in a `for await...of` loop. This provides each item in the directory as either a `FileDropItem` or` DirectoryDropItem `, and you can access the contents of each file as discussed above. This example accepts directory drops over the whole collection, and renders the contents as items in the list. `DIRECTORY_DRAG_TYPE` is imported from `react-aria-components` and included in the `acceptedDragTypes` prop to limit the accepted items to only directories. import File from '@spectrum-icons/workflow/FileTxt'; import Folder from '@spectrum-icons/workflow/Folder'; import {DIRECTORY_DRAG_TYPE, isDirectoryDropItem} from 'react-aria-components'; interface DirItem { name: string; kind: string; } function Example() { let [files, setFiles] = React.useState<DirItem[]>([]); let { dragAndDropHooks } = useDragAndDrop({ acceptedDragTypes: [DIRECTORY_DRAG_TYPE], async onRootDrop(e) { // Read entries in directory and update state with relevant info. let dir = e.items.find(isDirectoryDropItem)!; let files = []; for await (let entry of dir.getEntries()) { files.push({ name: entry.name, kind: entry.kind }); } setFiles(files); } }); return ( <ListBox aria-label="Droppable list" items={files} dragAndDropHooks={dragAndDropHooks} renderEmptyState={() => 'Drop items here'} > {(item) => ( <ListBoxItem id={item.name} textValue={item.name}> <div className="dir-item"> {item.kind === 'directory' ? <Folder /> : <File />} <span>{item.name}</span> </div> </ListBoxItem> )} </ListBox> ); } import File from '@spectrum-icons/workflow/FileTxt'; import Folder from '@spectrum-icons/workflow/Folder'; import { DIRECTORY_DRAG_TYPE, isDirectoryDropItem } from 'react-aria-components'; interface DirItem { name: string; kind: string; } function Example() { let [files, setFiles] = React.useState<DirItem[]>([]); let { dragAndDropHooks } = useDragAndDrop({ acceptedDragTypes: [DIRECTORY_DRAG_TYPE], async onRootDrop(e) { // Read entries in directory and update state with relevant info. let dir = e.items.find(isDirectoryDropItem)!; let files = []; for await (let entry of dir.getEntries()) { files.push({ name: entry.name, kind: entry.kind }); } setFiles(files); } }); return ( <ListBox aria-label="Droppable list" items={files} dragAndDropHooks={dragAndDropHooks} renderEmptyState={() => 'Drop items here'} > {(item) => ( <ListBoxItem id={item.name} textValue={item.name}> <div className="dir-item"> {item.kind === 'directory' ? <Folder /> : <File />} <span>{item.name}</span> </div> </ListBoxItem> )} </ListBox> ); } import File from '@spectrum-icons/workflow/FileTxt'; import Folder from '@spectrum-icons/workflow/Folder'; import { DIRECTORY_DRAG_TYPE, isDirectoryDropItem } from 'react-aria-components'; interface DirItem { name: string; kind: string; } function Example() { let [files, setFiles] = React.useState< DirItem[] >([]); let { dragAndDropHooks } = useDragAndDrop({ acceptedDragTypes: [ DIRECTORY_DRAG_TYPE ], async onRootDrop(e) { // Read entries in directory and update state with relevant info. let dir = e.items .find( isDirectoryDropItem )!; let files = []; for await ( let entry of dir .getEntries() ) { files.push({ name: entry.name, kind: entry.kind }); } setFiles(files); } }); return ( <ListBox aria-label="Droppable list" items={files} dragAndDropHooks={dragAndDropHooks} renderEmptyState={() => 'Drop items here'} > {(item) => ( <ListBoxItem id={item.name} textValue={item .name} > <div className="dir-item"> {item .kind === 'directory' ? ( <Folder /> ) : <File />} <span> {item.name} </span> </div> </ListBoxItem> )} </ListBox> ); } Drop items here Show CSS .dir-item { display: flex; align-items: center; gap: 8px; } .dir-item { flex: 0 0 auto; } .dir-item { flex: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .dir-item { display: flex; align-items: center; gap: 8px; } .dir-item { flex: 0 0 auto; } .dir-item { flex: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .dir-item { display: flex; align-items: center; gap: 8px; } .dir-item { flex: 0 0 auto; } .dir-item { flex: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } ### Drop operations# A `DropOperation` is an indication of what will happen when dragged data is dropped on a particular drop target. These are: * `move` – indicates that the dragged data will be moved from its source location to the target location. * `copy` – indicates that the dragged data will be copied to the target destination. * `link` – indicates that there will be a relationship established between the source and target locations. * `cancel` – indicates that the drag and drop operation will be canceled, resulting in no changes made to the source or target. Many operating systems display these in the form of a cursor change, e.g. a plus sign to indicate a copy operation. The user may also be able to use a modifier key to choose which drop operation to perform, such as Option or Alt to switch from move to copy. #### onDragEnd# The `onDragEnd` event allows the drag source to respond when a drag that it initiated ends, either because it was dropped or because it was canceled by the user. The `dropOperation` property of the event object indicates the operation that was performed. For example, when data is moved, the UI could be updated to reflect this change by removing the original dragged items. This example removes the dragged items from the UI when a move operation is completed. Try holding the Option or Alt keys to change the operation to copy, and see how the behavior changes. function Example() { let list = useListData({ initialItems: [ { id: 1, name: 'Adobe Photoshop' }, { id: 2, name: 'Adobe XD' }, { id: 3, name: 'Adobe Dreamweaver' }, { id: 4, name: 'Adobe InDesign' }, { id: 5, name: 'Adobe Connect' } ] }); let { dragAndDropHooks } = useDragAndDrop({ // ... onDragEnd(e) { if (e.dropOperation === 'move') { list.remove(...e.keys); } } }); return ( <div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }}> <ListBox aria-label="Draggable list" selectionMode="multiple" items={list.items} dragAndDropHooks={dragAndDropHooks} > {(item) => <ListBoxItem>{item.name}</ListBoxItem>} </ListBox> <DroppableListBox /> </div> ); } function Example() { let list = useListData({ initialItems: [ { id: 1, name: 'Adobe Photoshop' }, { id: 2, name: 'Adobe XD' }, { id: 3, name: 'Adobe Dreamweaver' }, { id: 4, name: 'Adobe InDesign' }, { id: 5, name: 'Adobe Connect' } ] }); let { dragAndDropHooks } = useDragAndDrop({ // ... onDragEnd(e) { if (e.dropOperation === 'move') { list.remove(...e.keys); } } }); return ( <div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }} > <ListBox aria-label="Draggable list" selectionMode="multiple" items={list.items} dragAndDropHooks={dragAndDropHooks} > {(item) => <ListBoxItem>{item.name}</ListBoxItem>} </ListBox> <DroppableListBox /> </div> ); } function Example() { let list = useListData( { initialItems: [ { id: 1, name: 'Adobe Photoshop' }, { id: 2, name: 'Adobe XD' }, { id: 3, name: 'Adobe Dreamweaver' }, { id: 4, name: 'Adobe InDesign' }, { id: 5, name: 'Adobe Connect' } ] } ); let { dragAndDropHooks } = useDragAndDrop({ // ... onDragEnd(e) { if ( e.dropOperation === 'move' ) { list.remove( ...e.keys ); } } }); return ( <div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }} > <ListBox aria-label="Draggable list" selectionMode="multiple" items={list .items} dragAndDropHooks={dragAndDropHooks} > {(item) => ( <ListBoxItem> {item.name} </ListBoxItem> )} </ListBox> <DroppableListBox /> </div> ); } Adobe Photoshop Adobe XD Adobe Dreamweaver Adobe InDesign Adobe Connect Drop items here #### getAllowedDropOperations# The drag source can also control which drop operations are allowed for the data. For example, if moving data is not allowed, and only copying is supported, the `getAllowedDropOperations` function could be implemented to indicate this. When you drag the element below, the cursor now shows the copy affordance by default, and pressing a modifier to switch drop operations results in the drop being canceled. function Example() { // ... let { dragAndDropHooks } = useDragAndDrop({ // ... getAllowedDropOperations: () => ['copy'] }); return ( <div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }}> <ListBox aria-label="Draggable list" selectionMode="multiple" items={list.items} dragAndDropHooks={dragAndDropHooks} > {(item) => <ListBoxItem>{item.name}</ListBoxItem>} </ListBox> <DroppableListBox /> </div> ); } function Example() { // ... let { dragAndDropHooks } = useDragAndDrop({ // ... getAllowedDropOperations: () => ['copy'] }); return ( <div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }} > <ListBox aria-label="Draggable list" selectionMode="multiple" items={list.items} dragAndDropHooks={dragAndDropHooks} > {(item) => <ListBoxItem>{item.name}</ListBoxItem>} </ListBox> <DroppableListBox /> </div> ); } function Example() { // ... let { dragAndDropHooks } = useDragAndDrop({ // ... getAllowedDropOperations: () => ['copy'] }); return ( <div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }} > <ListBox aria-label="Draggable list" selectionMode="multiple" items={list .items} dragAndDropHooks={dragAndDropHooks} > {(item) => ( <ListBoxItem> {item.name} </ListBoxItem> )} </ListBox> <DroppableListBox /> </div> ); } Adobe Photoshop Adobe XD Adobe Dreamweaver Adobe InDesign Adobe Connect Drop items here #### getDropOperation# The `getDropOperation` function passed to `useDragAndDrop` can be used to provide appropriate feedback to the user when a drag hovers over the drop target. This function receives the drop target, set of types contained in the drag, and a list of allowed drop operations as specified by the drag source. It should return one of the drop operations in `allowedOperations`, or a specific drop operation if only that drop operation is supported. It may also return `'cancel'` to reject the drop. If the returned operation is not in `allowedOperations`, then the drop target will act as if `'cancel'` was returned. In the below example, the drop target only supports dropping PNG images. If a PNG is dragged over the target, it will be highlighted and the operating system displays a copy cursor. If another type is dragged over the target, then there is no visual feedback, indicating that a drop is not accepted there. If the user holds a modifier key such as Control while dragging over the drop target in order to change the drop operation, then the drop target does not accept the drop. function Example() { let [items, setItems] = React.useState<ImageItem[]>([]); let { dragAndDropHooks } = useDragAndDrop({ getDropOperation: () => 'copy', acceptedDragTypes: ['image/png'], async onRootDrop(e) { // ... } }); // See "Files" example above... } function Example() { let [items, setItems] = React.useState<ImageItem[]>([]); let { dragAndDropHooks } = useDragAndDrop({ getDropOperation: () => 'copy', acceptedDragTypes: ['image/png'], async onRootDrop(e) { // ... } }); // See "Files" example above... } function Example() { let [items, setItems] = React.useState< ImageItem[] >([]); let { dragAndDropHooks } = useDragAndDrop({ getDropOperation: () => 'copy', acceptedDragTypes: [ 'image/png' ], async onRootDrop(e) { // ... } }); // See "Files" example above... } Drop items here #### Drop events# Drop events such as `onInsert`, `onItemDrop`, etc. also include the `dropOperation`. This can be used to perform different actions accordingly, for example, when communicating with a backend API. let onItemDrop = async (e) => { let data = JSON.parse(await e.items[0].getText('my-app-file')); switch (e.dropOperation) { case 'move': MyAppFileService.move(data.filePath, props.filePath); break; case 'copy': MyAppFileService.copy(data.filePath, props.filePath); break; case 'link': MyAppFileService.link(data.filePath, props.filePath); break; }}; let onItemDrop = async (e) => { let data = JSON.parse( await e.items[0].getText('my-app-file') ); switch (e.dropOperation) { case 'move': MyAppFileService.move(data.filePath, props.filePath); break; case 'copy': MyAppFileService.copy(data.filePath, props.filePath); break; case 'link': MyAppFileService.link(data.filePath, props.filePath); break; }}; let onItemDrop = async ( e ) => { let data = JSON.parse( await e.items[0] .getText( 'my-app-file' ) ); switch ( e.dropOperation ) { case 'move': MyAppFileService .move( data.filePath, props.filePath ); break; case 'copy': MyAppFileService .copy( data.filePath, props.filePath ); break; case 'link': MyAppFileService .link( data.filePath, props.filePath ); break; }}; ### Drag between lists# This example puts together many of the concepts described above, allowing users to drag items between lists bidirectionally. It also supports reordering items within the same list. When a list is empty, it accepts drops on the whole collection. `getDropOperation` ensures that items are always moved rather than copied, which avoids duplicate items. import {isTextDropItem} from 'react-aria-components'; interface FileItem { id: string, name: string, type: string } interface DndListBoxProps { initialItems: FileItem[], 'aria-label': string } function DndListBox(props: DndListBoxProps) { let list = useListData({ initialItems: props.initialItems }); let { dragAndDropHooks } = useDragAndDrop({ // Provide drag data in a custom format as well as plain text. getItems(keys) { return [...keys].map((key) => { let item = list.getItem(key); return { 'custom-app-type': JSON.stringify(item), 'text/plain': item.name }; }); }, // Accept drops with the custom format. acceptedDragTypes: ['custom-app-type'], // Ensure items are always moved rather than copied. getDropOperation: () => 'move', // Handle drops between items from other lists. async onInsert(e) { let processedItems = await Promise.all( e.items .filter(isTextDropItem) .map(async item => JSON.parse(await item.getText('custom-app-type'))) ); if (e.target.dropPosition === 'before') { list.insertBefore(e.target.key, ...processedItems); } else if (e.target.dropPosition === 'after') { list.insertAfter(e.target.key, ...processedItems); } }, // Handle drops on the collection when empty. async onRootDrop(e) { let processedItems = await Promise.all( e.items .filter(isTextDropItem) .map(async item => JSON.parse(await item.getText('custom-app-type'))) ); list.append(...processedItems); }, // Handle reordering items within the same list. onReorder(e) { if (e.target.dropPosition === 'before') { list.moveBefore(e.target.key, e.keys); } else if (e.target.dropPosition === 'after') { list.moveAfter(e.target.key, e.keys); } }, // Remove the items from the source list on drop // if they were moved to a different list. onDragEnd(e) { if (e.dropOperation === 'move' && !e.isInternal) { list.remove(...e.keys); } } }); return ( <ListBox aria-label={props['aria-label']} selectionMode="multiple" selectedKeys={list.selectedKeys} onSelectionChange={list.setSelectedKeys} items={list.items} dragAndDropHooks={dragAndDropHooks} renderEmptyState={() => 'Drop items here'}> {item => <ListBoxItem>{item.name}</ListBoxItem>} </ListBox> ); } <div style={{display: 'flex', gap: 12, flexWrap: 'wrap'}}> <DndListBox initialItems={[ { id: '1', type: 'file', name: 'Adobe Photoshop' }, { id: '2', type: 'file', name: 'Adobe XD' }, { id: '3', type: 'folder', name: 'Documents' }, { id: '4', type: 'file', name: 'Adobe InDesign' }, { id: '5', type: 'folder', name: 'Utilities' }, { id: '6', type: 'file', name: 'Adobe AfterEffects' } ]} aria-label="First ListBox" /> <DndListBox initialItems={[ { id: '7', type: 'folder', name: 'Pictures' }, { id: '8', type: 'file', name: 'Adobe Fresco' }, { id: '9', type: 'folder', name: 'Apps' }, { id: '10', type: 'file', name: 'Adobe Illustrator' }, { id: '11', type: 'file', name: 'Adobe Lightroom' }, { id: '12', type: 'file', name: 'Adobe Dreamweaver' } ]} aria-label="Second ListBox" /> </div> import {isTextDropItem} from 'react-aria-components'; interface FileItem { id: string; name: string; type: string; } interface DndListBoxProps { initialItems: FileItem[]; 'aria-label': string; } function DndListBox(props: DndListBoxProps) { let list = useListData({ initialItems: props.initialItems }); let { dragAndDropHooks } = useDragAndDrop({ // Provide drag data in a custom format as well as plain text. getItems(keys) { return [...keys].map((key) => { let item = list.getItem(key); return { 'custom-app-type': JSON.stringify(item), 'text/plain': item.name }; }); }, // Accept drops with the custom format. acceptedDragTypes: ['custom-app-type'], // Ensure items are always moved rather than copied. getDropOperation: () => 'move', // Handle drops between items from other lists. async onInsert(e) { let processedItems = await Promise.all( e.items .filter(isTextDropItem) .map(async (item) => JSON.parse( await item.getText('custom-app-type') ) ) ); if (e.target.dropPosition === 'before') { list.insertBefore(e.target.key, ...processedItems); } else if (e.target.dropPosition === 'after') { list.insertAfter(e.target.key, ...processedItems); } }, // Handle drops on the collection when empty. async onRootDrop(e) { let processedItems = await Promise.all( e.items .filter(isTextDropItem) .map(async (item) => JSON.parse( await item.getText('custom-app-type') ) ) ); list.append(...processedItems); }, // Handle reordering items within the same list. onReorder(e) { if (e.target.dropPosition === 'before') { list.moveBefore(e.target.key, e.keys); } else if (e.target.dropPosition === 'after') { list.moveAfter(e.target.key, e.keys); } }, // Remove the items from the source list on drop // if they were moved to a different list. onDragEnd(e) { if (e.dropOperation === 'move' && !e.isInternal) { list.remove(...e.keys); } } }); return ( <ListBox aria-label={props['aria-label']} selectionMode="multiple" selectedKeys={list.selectedKeys} onSelectionChange={list.setSelectedKeys} items={list.items} dragAndDropHooks={dragAndDropHooks} renderEmptyState={() => 'Drop items here'} > {(item) => <ListBoxItem>{item.name}</ListBoxItem>} </ListBox> ); } <div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }} > <DndListBox initialItems={[ { id: '1', type: 'file', name: 'Adobe Photoshop' }, { id: '2', type: 'file', name: 'Adobe XD' }, { id: '3', type: 'folder', name: 'Documents' }, { id: '4', type: 'file', name: 'Adobe InDesign' }, { id: '5', type: 'folder', name: 'Utilities' }, { id: '6', type: 'file', name: 'Adobe AfterEffects' } ]} aria-label="First ListBox" /> <DndListBox initialItems={[ { id: '7', type: 'folder', name: 'Pictures' }, { id: '8', type: 'file', name: 'Adobe Fresco' }, { id: '9', type: 'folder', name: 'Apps' }, { id: '10', type: 'file', name: 'Adobe Illustrator' }, { id: '11', type: 'file', name: 'Adobe Lightroom' }, { id: '12', type: 'file', name: 'Adobe Dreamweaver' } ]} aria-label="Second ListBox" /> </div> import {isTextDropItem} from 'react-aria-components'; interface FileItem { id: string; name: string; type: string; } interface DndListBoxProps { initialItems: FileItem[]; 'aria-label': string; } function DndListBox( props: DndListBoxProps ) { let list = useListData( { initialItems: props .initialItems } ); let { dragAndDropHooks } = useDragAndDrop({ // Provide drag data in a custom format as well as plain text. getItems(keys) { return [...keys] .map((key) => { let item = list .getItem( key ); return { 'custom-app-type': JSON .stringify( item ), 'text/plain': item.name }; }); }, // Accept drops with the custom format. acceptedDragTypes: [ 'custom-app-type' ], // Ensure items are always moved rather than copied. getDropOperation: () => 'move', // Handle drops between items from other lists. async onInsert(e) { let processedItems = await Promise .all( e.items .filter( isTextDropItem ) .map( async (item) => JSON .parse( await item .getText( 'custom-app-type' ) ) ) ); if ( e.target .dropPosition === 'before' ) { list .insertBefore( e.target.key, ...processedItems ); } else if ( e.target .dropPosition === 'after' ) { list.insertAfter( e.target.key, ...processedItems ); } }, // Handle drops on the collection when empty. async onRootDrop(e) { let processedItems = await Promise .all( e.items .filter( isTextDropItem ) .map( async (item) => JSON .parse( await item .getText( 'custom-app-type' ) ) ) ); list.append( ...processedItems ); }, // Handle reordering items within the same list. onReorder(e) { if ( e.target .dropPosition === 'before' ) { list.moveBefore( e.target.key, e.keys ); } else if ( e.target .dropPosition === 'after' ) { list.moveAfter( e.target.key, e.keys ); } }, // Remove the items from the source list on drop // if they were moved to a different list. onDragEnd(e) { if ( e.dropOperation === 'move' && !e.isInternal ) { list.remove( ...e.keys ); } } }); return ( <ListBox aria-label={props[ 'aria-label' ]} selectionMode="multiple" selectedKeys={list .selectedKeys} onSelectionChange={list .setSelectedKeys} items={list.items} dragAndDropHooks={dragAndDropHooks} renderEmptyState={() => 'Drop items here'} > {(item) => ( <ListBoxItem> {item.name} </ListBoxItem> )} </ListBox> ); } <div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }} > <DndListBox initialItems={[ { id: '1', type: 'file', name: 'Adobe Photoshop' }, { id: '2', type: 'file', name: 'Adobe XD' }, { id: '3', type: 'folder', name: 'Documents' }, { id: '4', type: 'file', name: 'Adobe InDesign' }, { id: '5', type: 'folder', name: 'Utilities' }, { id: '6', type: 'file', name: 'Adobe AfterEffects' } ]} aria-label="First ListBox" /> <DndListBox initialItems={[ { id: '7', type: 'folder', name: 'Pictures' }, { id: '8', type: 'file', name: 'Adobe Fresco' }, { id: '9', type: 'folder', name: 'Apps' }, { id: '10', type: 'file', name: 'Adobe Illustrator' }, { id: '11', type: 'file', name: 'Adobe Lightroom' }, { id: '12', type: 'file', name: 'Adobe Dreamweaver' } ]} aria-label="Second ListBox" /> </div> Adobe Photoshop Adobe XD Documents Adobe InDesign Utilities Adobe AfterEffects Pictures Adobe Fresco Apps Adobe Illustrator Adobe Lightroom Adobe Dreamweaver ## Props# * * * ### ListBox# | Name | Type | Default | Description | | --- | --- | --- | --- | | `selectionBehavior` | ` SelectionBehavior ` | — | How multiple selection should behave in the collection. | | `dragAndDropHooks` | ` DragAndDropHooks ` | — | The drag and drop hooks returned by `useDragAndDrop` used to enable drag and drop behavior for the ListBox. | | `renderEmptyState` | `( (props: ListBoxRenderProps )) => ReactNode` | — | Provides content to display when there are no items in the list. | | `layout` | `'stack' | 'grid'` | `'stack'` | Whether the items are arranged in a stack or grid. | | `orientation` | ` Orientation ` | `'vertical'` | The primary orientation of the items. Usually this is the direction that the collection scrolls. | | `shouldSelectOnPressUp` | `boolean` | — | Whether selection should occur on press up instead of press down. | | `shouldFocusOnHover` | `boolean` | — | Whether options should be focused when the user hovers over them. | | `escapeKeyBehavior` | `'clearSelection' | 'none'` | `'clearSelection'` | Whether pressing the escape key should clear selection in the listbox or not. Most experiences should not modify this option as it eliminates a keyboard user's ability to easily clear selection. Only use if the escape key is being handled externally or should not trigger selection clearing contextually. | | `autoFocus` | `boolean | FocusStrategy ` | — | Whether to auto focus the listbox or an option. | | `shouldFocusWrap` | `boolean` | — | Whether focus should wrap around when the end/start is reached. | | `items` | `Iterable<T>` | — | Item objects in the collection. | | `disabledKeys` | `Iterable<Key>` | — | The item keys that are disabled. These items cannot be selected, focused, or otherwise interacted with. | | `selectionMode` | ` SelectionMode ` | — | The type of selection that is allowed in the collection. | | `disallowEmptySelection` | `boolean` | — | Whether the collection allows empty selection. | | `selectedKeys` | `'all' | Iterable<Key>` | — | The currently selected keys in the collection (controlled). | | `defaultSelectedKeys` | `'all' | Iterable<Key>` | — | The initial selected keys in the collection (uncontrolled). | | `children` | `ReactNode | ( (item: object )) => ReactNode` | — | The contents of the collection. | | `dependencies` | `ReadonlyArray<any>` | — | Values that should invalidate the item cache when using dynamic collections. | | `className` | `string | ( (values: ListBoxRenderProps & & { defaultClassName: string | | undefined } )) => string` | — | The CSS className for the element. A function may be provided to compute the class based on component state. | | `style` | `CSSProperties | ( (values: ListBoxRenderProps & & { defaultStyle: CSSProperties } )) => CSSProperties | undefined` | — | The inline style for the element. A function may be provided to compute the style based on component state. | Events | Name | Type | Description | | --- | --- | --- | | `onAction` | `( (key: Key )) => void` | Handler that is called when a user performs an action on an item. The exact user event depends on the collection's `selectionBehavior` prop and the interaction modality. | | `onSelectionChange` | `( (keys: Selection )) => void` | Handler that is called when the selection changes. | | `onFocus` | `( (e: FocusEvent<Target> )) => void` | Handler that is called when the element receives focus. | | `onBlur` | `( (e: FocusEvent<Target> )) => void` | Handler that is called when the element loses focus. | | `onFocusChange` | `( (isFocused: boolean )) => void` | Handler that is called when the element's focus status changes. | | `onScroll` | `( (e: UIEvent<Element> )) => void` | Handler that is called when a user scrolls. See MDN. | Layout | Name | Type | Description | | --- | --- | --- | | `slot` | `string | null` | A slot name for the component. Slots allow the component to receive props from a parent component. An explicit `null` value indicates that the local props completely override all props received from a parent. | Accessibility | Name | Type | Description | | --- | --- | --- | | `id` | `string` | The element's unique identifier. See MDN. | | `aria-label` | `string` | Defines a string value that labels the current element. | | `aria-labelledby` | `string` | Identifies the element (or elements) that labels the current element. | | `aria-describedby` | `string` | Identifies the element (or elements) that describes the object. | | `aria-details` | `string` | Identifies the element (or elements) that provide a detailed, extended description for the object. | ### ListBoxSection# A `<ListBoxSection>` defines the child items for a section within a `<ListBox>`. It may also contain an optional `<Header>` element. If there is no header, then an `aria-label` must be provided to identify the section to assistive technologies. Show props | Name | Type | Description | | --- | --- | --- | | `id` | `Key` | The unique id of the section. | | `value` | `object` | The object value that this section represents. When using dynamic collections, this is set automatically. | | `children` | `ReactNode | ( (item: object )) => ReactElement` | Static child items or a function to render children. | | `dependencies` | `ReadonlyArray<any>` | Values that should invalidate the item cache when using dynamic collections. | | `items` | `Iterable<object>` | Item objects in the section. | | `className` | `string` | The CSS className for the element. | | `style` | `CSSProperties` | The inline style for the element. | Accessibility | Name | Type | Description | | --- | --- | --- | | `aria-label` | `string` | An accessibility label for the section. | ### Header# A `<Header>` defines the title for a `<ListBoxSection>`. It accepts all DOM attributes. ### ListBoxItem# A `<ListBoxItem>` defines a single option within a `<ListBox>`. If the `children` are not plain text, then the `textValue` prop must also be set to a plain text representation, which will be used for typeahead in the ListBox. Show props | Name | Type | Description | | --- | --- | --- | | `id` | `Key` | The unique id of the item. | | `value` | `object` | The object value that this item represents. When using dynamic collections, this is set automatically. | | `textValue` | `string` | A string representation of the item's contents, used for features like typeahead. | | `isDisabled` | `boolean` | Whether the item is disabled. | | `children` | `ReactNode | ( (values: ListBoxItemRenderProps & & { defaultChildren: ReactNode | | undefined } )) => ReactNode` | The children of the component. A function may be provided to alter the children based on component state. | | `className` | `string | ( (values: ListBoxItemRenderProps & & { defaultClassName: string | | undefined } )) => string` | The CSS className for the element. A function may be provided to compute the class based on component state. | | `style` | `CSSProperties | ( (values: ListBoxItemRenderProps & & { defaultStyle: CSSProperties } )) => CSSProperties | undefined` | The inline style for the element. A function may be provided to compute the style based on component state. | | `href` | ` Href ` | A URL to link to. See MDN. | | `hrefLang` | `string` | Hints at the human language of the linked URL. SeeMDN. | | `target` | `HTMLAttributeAnchorTarget` | The target window for the link. See MDN. | | `rel` | `string` | The relationship between the linked resource and the current page. See MDN. | | `download` | `boolean | string` | Causes the browser to download the linked URL. A string may be provided to suggest a file name. See MDN. | | `ping` | `string` | A space-separated list of URLs to ping when the link is followed. See MDN. | | `referrerPolicy` | `HTMLAttributeReferrerPolicy` | How much of the referrer to send when following the link. See MDN. | | `routerOptions` | ` RouterOptions ` | Options for the configured client side router. | Events | Name | Type | Description | | --- | --- | --- | | `onAction` | `() => void` | Handler that is called when a user performs an action on the item. The exact user event depends on the collection's `selectionBehavior` prop and the interaction modality. | | `onHoverStart` | `( (e: HoverEvent )) => void` | Handler that is called when a hover interaction starts. | | `onHoverEnd` | `( (e: HoverEvent )) => void` | Handler that is called when a hover interaction ends. | | `onHoverChange` | `( (isHovering: boolean )) => void` | Handler that is called when the hover state changes. | Accessibility | Name | Type | Description | | --- | --- | --- | | `aria-label` | `string` | An accessibility label for this item. | ## Styling# * * * React Aria components can be styled in many ways, including using CSS classes, inline styles, utility classes (e.g. Tailwind), CSS-in-JS (e.g. Styled Components), etc. By default, all components include a builtin `className` attribute which can be targeted using CSS selectors. These follow the `react-aria-ComponentName` naming convention. .react-aria-ListBox { /* ... */ } .react-aria-ListBox { /* ... */ } .react-aria-ListBox { /* ... */ } A custom `className` can also be specified on any component. This overrides the default `className` provided by React Aria with your own. <ListBox className="my-listbox"> {/* ... */} </ListBox> <ListBox className="my-listbox"> {/* ... */} </ListBox> <ListBox className="my-listbox"> {/* ... */} </ListBox> In addition, some components support multiple UI states (e.g. pressed, hovered, etc.). React Aria components expose states using data attributes, which you can target in CSS selectors. 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] { /* ... */ } The `className` and `style` props also accept functions which receive states for styling. This lets you dynamically determine the classes or styles to apply, which is useful when using utility CSS libraries like Tailwind. <ListBoxItem className={({ isSelected }) => isSelected ? 'bg-blue-400' : 'bg-gray-100'} > Item </ListBoxItem> <ListBoxItem className={({ isSelected }) => isSelected ? 'bg-blue-400' : 'bg-gray-100'} > Item </ListBoxItem> <ListBoxItem className={( { isSelected } ) => isSelected ? 'bg-blue-400' : 'bg-gray-100'} > 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 />} Item </> )} </ListBoxItem> <ListBoxItem> {({isSelected}) => ( <> {isSelected && <CheckmarkIcon />} Item </> )} </ListBoxItem> <ListBoxItem> {( { isSelected } ) => ( <> {isSelected && ( <CheckmarkIcon /> )} Item </> )} </ListBoxItem> The states and selectors for each component used in a `ListBox` are documented below. ### ListBox# A `ListBox` can be targeted with the `.react-aria-ListBox` CSS selector, or by overriding with a custom `className`. It supports the following states and render props: | Name | CSS Selector | Description | | --- | --- | --- | | `isEmpty` | `[data-empty]` | Whether the listbox has no items and should display its empty state. | | `isFocused` | `[data-focused]` | Whether the listbox is currently focused. | | `isFocusVisible` | `[data-focus-visible]` | Whether the listbox is currently keyboard focused. | | `isDropTarget` | `[data-drop-target]` | Whether the listbox is currently the active drop target. | | `layout` | `[data-layout="stack | grid"]` | Whether the items are arranged in a stack or grid. | | `state` | `—` | State of the listbox. | ### ListBoxSection# A `ListBoxSection` can be targeted with the `.react-aria-ListBoxSection` CSS selector, or by overriding with a custom `className`. See sections for examples. ### Header# A `Header` within a `ListBoxSection` can be targeted with the `.react-aria-Header` CSS selector, or by overriding with a custom `className`. See sections for examples. ### ListBoxItem# A `ListBoxItem` can be targeted with the `.react-aria-ListBoxItem` CSS selector, or by overriding with a custom `className`. It supports the following states and render props: | Name | CSS Selector | Description | | --- | --- | --- | | `isHovered` | `[data-hovered]` | Whether the item is currently hovered with a mouse. | | `isPressed` | `[data-pressed]` | Whether the item is currently in a pressed state. | | `isSelected` | `[data-selected]` | Whether the item is currently selected. | | `isFocused` | `[data-focused]` | Whether the item is currently focused. | | `isFocusVisible` | `[data-focus-visible]` | Whether the item is currently keyboard focused. | | `isDisabled` | `[data-disabled]` | Whether the item is non-interactive, i.e. both selection and actions are disabled and the item may not be focused. Dependent on `disabledKeys` and `disabledBehavior`. | | `selectionMode` | `[data-selection-mode="single | multiple"]` | The type of selection that is allowed in the collection. | | `selectionBehavior` | `—` | The selection behavior for the collection. | | `allowsDragging` | `[data-allows-dragging]` | Whether the item allows dragging. | | `isDragging` | `[data-dragging]` | Whether the item is currently being dragged. | | `isDropTarget` | `[data-drop-target]` | Whether the item is currently an active drop target. | Items also support two slots: a label, and a description. When provided using the `<Text>` element, the item will have `aria-labelledby` and `aria-describedby` attributes pointing to these slots, improving screen reader announcement. See Text slots for an example. Note that items may not contain interactive children such as buttons, as screen readers will not be able to access them. ## Advanced customization# * * * ### Composition# If you need to customize one of the components within a `ListBox`, such as `ListBoxItem` or `ListBoxSection`, in many cases you can create a wrapper component. This lets you customize the props passed to the component. function MyItem(props) { return <ListBoxItem {...props} className="my-item" /> } function MyItem(props) { return <ListBoxItem {...props} className="my-item" /> } function MyItem(props) { return ( <ListBoxItem {...props} className="my-item" /> ); } ### Contexts# All React Aria Components export a corresponding context that can be used to send props to them from a parent element. This enables you to build your own compositional APIs similar to those found in React Aria Components itself. You can send any prop or ref via context that you could pass to the corresponding component. The local props and ref on the component are merged with the ones passed via context, with the local props taking precedence (following the rules documented in mergeProps). | Component | Context | Props | Ref | | --- | --- | --- | --- | | `ListBox` | `ListBoxContext` | ` ListBoxProps ` | `HTMLDivElement` | This example shows a component that accepts a `ListBox` and a Checkbox as children, and allows the user to select or deselect all items by toggling the checkbox. import {CheckboxContext, ListBoxContext} from 'react-aria-components'; function SelectAllListBox({children}) { let [selectedKeys, onSelectionChange] = React.useState<Selection>(new Set()); let isSelected = selectedKeys === 'all'; let onChange = (isSelected: boolean) => { onSelectionChange(isSelected ? 'all' : new Set()); }; return ( <CheckboxContext.Provider value={{isSelected, onChange}}> <ListBoxContext.Provider value={{selectedKeys, onSelectionChange}}> {children} </ListBoxContext.Provider> </CheckboxContext.Provider> ); } import { CheckboxContext, ListBoxContext } from 'react-aria-components'; function SelectAllListBox({ children }) { let [selectedKeys, onSelectionChange] = React.useState< Selection >(new Set()); let isSelected = selectedKeys === 'all'; let onChange = (isSelected: boolean) => { onSelectionChange(isSelected ? 'all' : new Set()); }; return ( <CheckboxContext.Provider value={{ isSelected, onChange }} > <ListBoxContext.Provider value={{ selectedKeys, onSelectionChange }} > {children} </ListBoxContext.Provider> </CheckboxContext.Provider> ); } import { CheckboxContext, ListBoxContext } from 'react-aria-components'; function SelectAllListBox( { children } ) { let [ selectedKeys, onSelectionChange ] = React.useState< Selection >(new Set()); let isSelected = selectedKeys === 'all'; let onChange = ( isSelected: boolean ) => { onSelectionChange( isSelected ? 'all' : new Set() ); }; return ( <CheckboxContext.Provider value={{ isSelected, onChange }} > <ListBoxContext.Provider value={{ selectedKeys, onSelectionChange }} > {children} </ListBoxContext.Provider> </CheckboxContext.Provider> ); } The `SelectAllListBox` component can be reused to allow the user to toggle select all for any nested `ListBox`. import {Checkbox} from 'react-aria-components'; <SelectAllListBox> <Checkbox style={{marginBottom: '8px'}}> <div className="checkbox"> <svg viewBox="0 0 18 18" aria-hidden="true"> <polyline points="1 9 7 14 15 4" /> </svg> </div> Unsubscribe </Checkbox> <ListBox selectionMode="multiple" aria-label="Ice cream flavors"> <ListBoxItem>Chocolate</ListBoxItem> <ListBoxItem>Mint</ListBoxItem> <ListBoxItem>Strawberry</ListBoxItem> <ListBoxItem>Vanilla</ListBoxItem> </ListBox> </SelectAllListBox> import {Checkbox} from 'react-aria-components'; <SelectAllListBox> <Checkbox style={{ marginBottom: '8px' }}> <div className="checkbox"> <svg viewBox="0 0 18 18" aria-hidden="true"> <polyline points="1 9 7 14 15 4" /> </svg> </div> Unsubscribe </Checkbox> <ListBox selectionMode="multiple" aria-label="Ice cream flavors" > <ListBoxItem>Chocolate</ListBoxItem> <ListBoxItem>Mint</ListBoxItem> <ListBoxItem>Strawberry</ListBoxItem> <ListBoxItem>Vanilla</ListBoxItem> </ListBox> </SelectAllListBox> import {Checkbox} from 'react-aria-components'; <SelectAllListBox> <Checkbox style={{ marginBottom: '8px' }} > <div className="checkbox"> <svg viewBox="0 0 18 18" aria-hidden="true" > <polyline points="1 9 7 14 15 4" /> </svg> </div> Unsubscribe </Checkbox> <ListBox selectionMode="multiple" aria-label="Ice cream flavors" > <ListBoxItem> Chocolate </ListBoxItem> <ListBoxItem> Mint </ListBoxItem> <ListBoxItem> Strawberry </ListBoxItem> <ListBoxItem> Vanilla </ListBoxItem> </ListBox> </SelectAllListBox> Unsubscribe Chocolate Mint Strawberry Vanilla ### Custom children# ListBox passes props to its child components, such as the section headers and separators, via their associated contexts. These contexts are exported so you can also consume them in your own custom components. This enables you to reuse existing components from your app or component library together with React Aria Components. | Component | Context | Props | Ref | | --- | --- | --- | --- | | `Separator` | `SeparatorContext` | ` SeparatorProps ` | `HTMLElement` | | `Header` | `HeaderContext` | `HTMLAttributes` | `HTMLElement` | | `Text` | `TextContext` | ` TextProps ` | `HTMLElement` | This example consumes from `TextContext` in an existing styled typography component to make it compatible with React Aria Components. The `useContextProps` hook merges the local props and ref with the ones provided via context by ListBox. import type {TextProps} from 'react-aria-components'; import {TextContext, useContextProps} from 'react-aria-components'; const MyText = React.forwardRef( (props: TextProps, ref: React.ForwardedRef<HTMLElement>) => { // Merge the local props and ref with the ones provided via context. [props, ref] = useContextProps(props, ref, TextContext); // ... your existing Text component return <span {...props} ref={ref} />; } ); import type {TextProps} from 'react-aria-components'; import { TextContext, useContextProps } from 'react-aria-components'; const MyText = React.forwardRef( ( props: TextProps, ref: React.ForwardedRef<HTMLElement> ) => { // Merge the local props and ref with the ones provided via context. [props, ref] = useContextProps(props, ref, TextContext); // ... your existing Text component return <span {...props} ref={ref} />; } ); import type {TextProps} from 'react-aria-components'; import { TextContext, useContextProps } from 'react-aria-components'; const MyText = React .forwardRef( ( props: TextProps, ref: React.ForwardedRef< HTMLElement > ) => { // Merge the local props and ref with the ones provided via context. [props, ref] = useContextProps( props, ref, TextContext ); // ... your existing Text component return ( <span {...props} ref={ref} /> ); } ); Now you can use `MyText` within a `Menu`, in place of the builtin React Aria Components `Text`. <ListBox> <ListBoxItem> <MyText slot="label">Option</MyText> </ListBoxItem> {/* ... */} </ListBox> <ListBox> <ListBoxItem> <MyText slot="label">Option</MyText> </ListBoxItem> {/* ... */} </ListBox> <ListBox> <ListBoxItem> <MyText slot="label"> Option </MyText> </ListBoxItem> {/* ... */} </ListBox> ### Hooks# If you need to customize things even further, such as accessing internal state or customizing DOM structure, you can drop down to the lower level Hook-based API. See useListBox for more details. ## Testing# * * * ### Test utils alpha# `@react-aria/test-utils` offers common listbox interaction utilities which you may find helpful when writing tests. See here for more information on how to setup these utilities in your tests. Below is the full definition of the listbox tester and a sample of how you could use it in your test suite. // ListBox.test.ts import {render} from '@testing-library/react'; import {User} from '@react-aria/test-utils'; let testUtilUser = new User({ interactionType: 'mouse' }); // ... it('ListBox can select an option via keyboard', async function () { // Render your test component/app and initialize the listbox tester let { getByTestId } = render( <ListBox selectionMode="single" data-testid="test-listbox"> ... </ListBox> ); let listboxTester = testUtilUser.createTester('ListBox', { root: getByTestId('test-listbox'), interactionType: 'keyboard' }); await listboxTester.toggleOptionSelection({ option: 4 }); expect(listboxTester.options()[4]).toHaveAttribute('aria-selected', 'true'); }); // ListBox.test.ts import {render} from '@testing-library/react'; import {User} from '@react-aria/test-utils'; let testUtilUser = new User({ interactionType: 'mouse' }); // ... it('ListBox can select an option via keyboard', async function () { // Render your test component/app and initialize the listbox tester let { getByTestId } = render( <ListBox selectionMode="single" data-testid="test-listbox" > ... </ListBox> ); let listboxTester = testUtilUser.createTester('ListBox', { root: getByTestId('test-listbox'), interactionType: 'keyboard' }); await listboxTester.toggleOptionSelection({ option: 4 }); expect(listboxTester.options()[4]).toHaveAttribute( 'aria-selected', 'true' ); }); // ListBox.test.ts import {render} from '@testing-library/react'; import {User} from '@react-aria/test-utils'; let testUtilUser = new User({ interactionType: 'mouse' }); // ... it('ListBox can select an option via keyboard', async function () { // Render your test component/app and initialize the listbox tester let { getByTestId } = render( <ListBox selectionMode="single" data-testid="test-listbox" > ... </ListBox> ); let listboxTester = testUtilUser .createTester( 'ListBox', { root: getByTestId( 'test-listbox' ), interactionType: 'keyboard' } ); await listboxTester .toggleOptionSelection( { option: 4 } ); expect( listboxTester .options()[4] ).toHaveAttribute( 'aria-selected', 'true' ); }); ### Properties | Name | Type | Description | | --- | --- | --- | | `listbox` | `HTMLElement` | Returns the listbox. | | `selectedOptions` | `HTMLElement[]` | Returns the listbox's selected options if any. | | `sections` | `HTMLElement[]` | Returns the listbox's sections if any. | ### Methods | Method | Description | | --- | --- | | `constructor( (opts: ListBoxTesterOpts )): void` | | | `setInteractionType( (type: UserOpts ['interactionType'] )): void` | Set the interaction type used by the listbox tester. | | `findOption( (opts: { optionIndexOrText: number | | string } )): HTMLElement` | Returns a option matching the specified index or text content. | | `toggleOptionSelection( (opts: ListBoxToggleOptionOpts )): Promise<void>` | Toggles the selection for the specified listbox option. Defaults to using the interaction type set on the listbox tester. | | `triggerOptionAction( (opts: ListBoxOptionActionOpts )): Promise<void>` | Triggers the action for the specified listbox option. Defaults to using the interaction type set on the listbox tester. | | `options( (opts: { element?: HTMLElement } )): HTMLElement[]` | Returns the listbox options. Can be filtered to a subsection of the listbox if provided via `element`. | --- ## Page: https://react-spectrum.adobe.com/react-aria/Menu.html # Menu A menu displays a list of actions or options that a user can choose. <table><tbody><tr><th>install</th><td><code>yarn add react-aria-components</code></td></tr><tr><th>version</th><td>1.10.1</td></tr><tr><th>usage</th><td><code><span>import</span> {MenuTrigger, Menu, SubmenuTrigger} <span>from</span> <span>'react-aria-components'</span></code></td></tr></tbody></table> View ARIA pattern W3C View repository GitHub View package NPM ## Example# * * * import {Button, Menu, MenuItem, MenuTrigger, Popover} from 'react-aria-components'; <MenuTrigger> <Button aria-label="Menu">☰</Button> <Popover> <Menu> <MenuItem onAction={() => alert('open')}>Open</MenuItem> <MenuItem onAction={() => alert('rename')}>Rename…</MenuItem> <MenuItem onAction={() => alert('duplicate')}>Duplicate</MenuItem> <MenuItem onAction={() => alert('share')}>Share…</MenuItem> <MenuItem onAction={() => alert('delete')}>Delete…</MenuItem> </Menu> </Popover> </MenuTrigger> import { Button, Menu, MenuItem, MenuTrigger, Popover } from 'react-aria-components'; <MenuTrigger> <Button aria-label="Menu">☰</Button> <Popover> <Menu> <MenuItem onAction={() => alert('open')}> Open </MenuItem> <MenuItem onAction={() => alert('rename')}> Rename… </MenuItem> <MenuItem onAction={() => alert('duplicate')}> Duplicate </MenuItem> <MenuItem onAction={() => alert('share')}> Share… </MenuItem> <MenuItem onAction={() => alert('delete')}> Delete… </MenuItem> </Menu> </Popover> </MenuTrigger> import { Button, Menu, MenuItem, MenuTrigger, Popover } from 'react-aria-components'; <MenuTrigger> <Button aria-label="Menu"> ☰ </Button> <Popover> <Menu> <MenuItem onAction={() => alert( 'open' )} > Open </MenuItem> <MenuItem onAction={() => alert( 'rename' )} > Rename… </MenuItem> <MenuItem onAction={() => alert( 'duplicate' )} > Duplicate </MenuItem> <MenuItem onAction={() => alert( 'share' )} > Share… </MenuItem> <MenuItem onAction={() => alert( 'delete' )} > Delete… </MenuItem> </Menu> </Popover> </MenuTrigger> ☰ Show CSS @import "@react-aria/example-theme"; .react-aria-Menu { max-height: inherit; box-sizing: border-box; overflow: auto; padding: 2px; min-width: 150px; box-sizing: border-box; outline: none; } .react-aria-MenuItem { margin: 2px; padding: 0.286rem 0.571rem; border-radius: 6px; outline: none; cursor: default; color: var(--text-color); font-size: 1.072rem; position: relative; display: grid; grid-template-areas: "label kbd" "desc kbd"; align-items: center; column-gap: 20px; forced-color-adjust: none; &[data-focused] { background: var(--highlight-background); color: var(--highlight-foreground); } } @import "@react-aria/example-theme"; .react-aria-Menu { max-height: inherit; box-sizing: border-box; overflow: auto; padding: 2px; min-width: 150px; box-sizing: border-box; outline: none; } .react-aria-MenuItem { margin: 2px; padding: 0.286rem 0.571rem; border-radius: 6px; outline: none; cursor: default; color: var(--text-color); font-size: 1.072rem; position: relative; display: grid; grid-template-areas: "label kbd" "desc kbd"; align-items: center; column-gap: 20px; forced-color-adjust: none; &[data-focused] { background: var(--highlight-background); color: var(--highlight-foreground); } } @import "@react-aria/example-theme"; .react-aria-Menu { max-height: inherit; box-sizing: border-box; overflow: auto; padding: 2px; min-width: 150px; box-sizing: border-box; outline: none; } .react-aria-MenuItem { margin: 2px; padding: 0.286rem 0.571rem; border-radius: 6px; outline: none; cursor: default; color: var(--text-color); font-size: 1.072rem; position: relative; display: grid; grid-template-areas: "label kbd" "desc kbd"; align-items: center; column-gap: 20px; forced-color-adjust: none; &[data-focused] { background: var(--highlight-background); color: var(--highlight-foreground); } } ## Features# * * * There is no native element to implement a menu in HTML that is widely supported. `MenuTrigger` and `Menu` help achieve accessible menu components that can be styled as needed. * **Keyboard navigation** – Menu items can be navigated using the arrow keys, along with page up/down, home/end, etc. Typeahead, auto scrolling, and disabled items are supported as well. * **Item selection** – Single or multiple selection can be optionally enabled. * **Trigger interactions** – Menus can be triggered by pressing with a mouse or touch, or optionally, with a long press interaction. The arrow keys also open the menu with a keyboard, automatically focusing the first or last item accordingly. * **Accessible** – Follows the ARIA menu pattern, with support for items and sections, and slots for label, description, and keyboard shortcut elements within each item for improved screen reader announcement. ## Anatomy# * * * A menu trigger consists of a button or other trigger element combined with a menu displayed in a popover, with a list of menu items or sections inside. Users can click, touch, or use the keyboard on the button to open the menu. import {Button, Header, Keyboard, Menu, MenuItem, MenuSection, MenuTrigger, Popover, Separator, Text} from 'react-aria-components'; <MenuTrigger> <Button /> <Popover> <Menu> <MenuItem> <Text slot="label" /> <Text slot="description" /> <Keyboard /> </MenuItem> <Separator /> <MenuSection> <Header /> <MenuItem /> </MenuSection> </Menu> </Popover> </MenuTrigger> import { Button, Header, Keyboard, Menu, MenuItem, MenuSection, MenuTrigger, Popover, Separator, Text } from 'react-aria-components'; <MenuTrigger> <Button /> <Popover> <Menu> <MenuItem> <Text slot="label" /> <Text slot="description" /> <Keyboard /> </MenuItem> <Separator /> <MenuSection> <Header /> <MenuItem /> </MenuSection> </Menu> </Popover> </MenuTrigger> import { Button, Header, Keyboard, Menu, MenuItem, MenuSection, MenuTrigger, Popover, Separator, Text } from 'react-aria-components'; <MenuTrigger> <Button /> <Popover> <Menu> <MenuItem> <Text slot="label" /> <Text slot="description" /> <Keyboard /> </MenuItem> <Separator /> <MenuSection> <Header /> <MenuItem /> </MenuSection> </Menu> </Popover> </MenuTrigger> ### Concepts# `Menu` makes use of the following concepts: Collections Defining collections of items, async loading, and updating items over time. Selection Interactions and data structures to represent selection. ### Composed components# A `Menu` uses the following components, which may also be used standalone or reused in other components. Button A button allows a user to perform an action. Popover A popover displays content in context with a trigger element. ## Examples# * * * Account Menu A Menu with an interactive header, built with a Dialog and Popover. Action Menu An animated menu of actions, styled with Tailwind CSS. Command Palette A command palette with actions, styled with Tailwind CSS. ## Starter kits# * * * To help kick-start your project, we offer starter kits that include example implementations of all React Aria components with various styling solutions. All components are fully styled, including support for dark mode, high contrast mode, and all UI states. Each starter comes with a pre-configured Storybook that you can experiment with, or use as a starting point for your own component library. Vanilla CSS Download ZIP Preview Tailwind CSS Download ZIP Preview ## Reusable wrappers# * * * If you will use a Menu in multiple places in your app, you can wrap all of the pieces into a reusable component. This way, the DOM structure, styling code, and other logic are defined in a single place and reused everywhere to ensure consistency. This example wraps `MenuTrigger` and all of its children together into a single component which accepts a `label` prop and `children`, which are passed through to the right places. The `MenuItem` component is also wrapped to apply class names based on the current state, as described in the styling section. import type {MenuItemProps, MenuProps, MenuTriggerProps} from 'react-aria-components'; interface MyMenuButtonProps<T> extends MenuProps<T>, Omit<MenuTriggerProps, 'children'> { label?: string; } function MyMenuButton<T extends object>( { label, children, ...props }: MyMenuButtonProps<T> ) { return ( <MenuTrigger {...props}> <Button>{label}</Button> <Popover> <Menu {...props}> {children} </Menu> </Popover> </MenuTrigger> ); } export function MyItem( props: Omit<MenuItemProps, 'children'> & { children?: React.ReactNode } ) { let textValue = props.textValue || (typeof props.children === 'string' ? props.children : undefined); return ( <MenuItem {...props} textValue={textValue} className={({ isFocused, isSelected, isOpen }) => `my-item ${isFocused ? 'focused' : ''} ${isOpen ? 'open' : ''}`} > {({ hasSubmenu }) => ( <> {props.children} {hasSubmenu && ( <svg className="chevron" viewBox="0 0 24 24"> <path d="m9 18 6-6-6-6" /> </svg> )} </> )} </MenuItem> ); } <MyMenuButton label="Edit"> <MyItem>Cut</MyItem> <MyItem>Copy</MyItem> <MyItem>Paste</MyItem> </MyMenuButton> import type { MenuItemProps, MenuProps, MenuTriggerProps } from 'react-aria-components'; interface MyMenuButtonProps<T> extends MenuProps<T>, Omit<MenuTriggerProps, 'children'> { label?: string; } function MyMenuButton<T extends object>( { label, children, ...props }: MyMenuButtonProps<T> ) { return ( <MenuTrigger {...props}> <Button>{label}</Button> <Popover> <Menu {...props}> {children} </Menu> </Popover> </MenuTrigger> ); } export function MyItem( props: Omit<MenuItemProps, 'children'> & { children?: React.ReactNode; } ) { let textValue = props.textValue || (typeof props.children === 'string' ? props.children : undefined); return ( <MenuItem {...props} textValue={textValue} className={({ isFocused, isSelected, isOpen }) => `my-item ${isFocused ? 'focused' : ''} ${ isOpen ? 'open' : '' }`} > {({ hasSubmenu }) => ( <> {props.children} {hasSubmenu && ( <svg className="chevron" viewBox="0 0 24 24"> <path d="m9 18 6-6-6-6" /> </svg> )} </> )} </MenuItem> ); } <MyMenuButton label="Edit"> <MyItem>Cut</MyItem> <MyItem>Copy</MyItem> <MyItem>Paste</MyItem> </MyMenuButton> import type { MenuItemProps, MenuProps, MenuTriggerProps } from 'react-aria-components'; interface MyMenuButtonProps< T > extends MenuProps<T>, Omit< MenuTriggerProps, 'children' > { label?: string; } function MyMenuButton< T extends object >( { label, children, ...props }: MyMenuButtonProps<T> ) { return ( <MenuTrigger {...props} > <Button> {label} </Button> <Popover> <Menu {...props}> {children} </Menu> </Popover> </MenuTrigger> ); } export function MyItem( props: & Omit< MenuItemProps, 'children' > & { children?: React.ReactNode; } ) { let textValue = props.textValue || (typeof props .children === 'string' ? props.children : undefined); return ( <MenuItem {...props} textValue={textValue} className={( { isFocused, isSelected, isOpen } ) => `my-item ${ isFocused ? 'focused' : '' } ${ isOpen ? 'open' : '' }`} > {( { hasSubmenu } ) => ( <> {props .children} {hasSubmenu && ( <svg className="chevron" viewBox="0 0 24 24" > <path d="m9 18 6-6-6-6" /> </svg> )} </> )} </MenuItem> ); } <MyMenuButton label="Edit"> <MyItem>Cut</MyItem> <MyItem>Copy</MyItem> <MyItem> Paste </MyItem> </MyMenuButton> Edit Show CSS .my-item { margin: 2px; padding: 0.286rem 0.571rem; border-radius: 6px; outline: none; cursor: default; color: var(--text-color); font-size: 1.072rem; position: relative; &.focused { background: #e70073; color: white; } &.open:not(.focused) { background: rgba(192, 192, 192, 0.3); color: var(--text-color); } .chevron { width: 20; height: 20; fill: none; stroke: currentColor; stroke-linecap: round; stroke-linejoin: round; stroke-width: 2; position: absolute; right: 0; top: 0; height: 100%; } } @media (forced-colors: active) { .my-item.focused { forced-color-adjust: none; background: Highlight; color: HighlightText; } } .my-item { margin: 2px; padding: 0.286rem 0.571rem; border-radius: 6px; outline: none; cursor: default; color: var(--text-color); font-size: 1.072rem; position: relative; &.focused { background: #e70073; color: white; } &.open:not(.focused) { background: rgba(192, 192, 192, 0.3); color: var(--text-color); } .chevron { width: 20; height: 20; fill: none; stroke: currentColor; stroke-linecap: round; stroke-linejoin: round; stroke-width: 2; position: absolute; right: 0; top: 0; height: 100%; } } @media (forced-colors: active) { .my-item.focused { forced-color-adjust: none; background: Highlight; color: HighlightText; } } .my-item { margin: 2px; padding: 0.286rem 0.571rem; border-radius: 6px; outline: none; cursor: default; color: var(--text-color); font-size: 1.072rem; position: relative; &.focused { background: #e70073; color: white; } &.open:not(.focused) { background: rgba(192, 192, 192, 0.3); color: var(--text-color); } .chevron { width: 20; height: 20; fill: none; stroke: currentColor; stroke-linecap: round; stroke-linejoin: round; stroke-width: 2; position: absolute; right: 0; top: 0; height: 100%; } } @media (forced-colors: active) { .my-item.focused { forced-color-adjust: none; background: Highlight; color: HighlightText; } } ## Content# * * * `Menu` follows the Collection Components API, accepting both static and dynamic collections. The examples above show static collections, which can be used when the full list of options is known ahead of time. Dynamic collections, as shown below, can be used when the options come from an external data source such as an API call, or update over time. As seen below, an iterable list of options is passed to the Menu using the `items` prop. Each item accepts an `id` prop, which is passed to the `onAction` prop on the `Menu` to identify the selected item. Alternatively, if the item objects contain an `id` property, as shown in the example below, then this is used automatically and an `id` prop is not required. function Example() { let items = [ {id: 1, name: 'New'}, {id: 2, name: 'Open'}, {id: 3, name: 'Close'}, {id: 4, name: 'Save'}, {id: 5, name: 'Duplicate'}, {id: 6, name: 'Rename'}, {id: 7, name: 'Move'} ]; return ( <MyMenuButton label="Actions" items={items} onAction={id => alert(id)}> {(item) => <MenuItem>{item.name}</MenuItem>} </MyMenuButton> ); } function Example() { let items = [ { id: 1, name: 'New' }, { id: 2, name: 'Open' }, { id: 3, name: 'Close' }, { id: 4, name: 'Save' }, { id: 5, name: 'Duplicate' }, { id: 6, name: 'Rename' }, { id: 7, name: 'Move' } ]; return ( <MyMenuButton label="Actions" items={items} onAction={(id) => alert(id)} > {(item) => <MenuItem>{item.name}</MenuItem>} </MyMenuButton> ); } function Example() { let items = [ { id: 1, name: 'New' }, { id: 2, name: 'Open' }, { id: 3, name: 'Close' }, { id: 4, name: 'Save' }, { id: 5, name: 'Duplicate' }, { id: 6, name: 'Rename' }, { id: 7, name: 'Move' } ]; return ( <MyMenuButton label="Actions" items={items} onAction={(id) => alert(id)} > {(item) => ( <MenuItem> {item.name} </MenuItem> )} </MyMenuButton> ); } Actions ## Selection# * * * Menu supports multiple selection modes. By default, selection is disabled, however this can be changed using the `selectionMode` prop. Use `defaultSelectedKeys` to provide a default set of selected items (uncontrolled) and `selectedKeys` to set the selected items (controlled). The value of the selected keys must match the `id` prop of the items. ### Single# import type {Selection} from 'react-aria-components'; function Example() { let [selected, setSelected] = React.useState<Selection>(new Set(['center'])); return ( <> <MyMenuButton label="Align" selectionMode="single" selectedKeys={selected} onSelectionChange={setSelected} > <MenuItem id="left">Left</MenuItem> <MenuItem id="center">Center</MenuItem> <MenuItem id="right">Right</MenuItem> </MyMenuButton> <p>Current selection (controlled): {[...selected].join(', ')}</p> </> ); } import type {Selection} from 'react-aria-components'; function Example() { let [selected, setSelected] = React.useState<Selection>( new Set(['center']) ); return ( <> <MyMenuButton label="Align" selectionMode="single" selectedKeys={selected} onSelectionChange={setSelected} > <MenuItem id="left">Left</MenuItem> <MenuItem id="center">Center</MenuItem> <MenuItem id="right">Right</MenuItem> </MyMenuButton> <p> Current selection (controlled):{' '} {[...selected].join(', ')} </p> </> ); } import type {Selection} from 'react-aria-components'; function Example() { let [ selected, setSelected ] = React.useState< Selection >(new Set(['center'])); return ( <> <MyMenuButton label="Align" selectionMode="single" selectedKeys={selected} onSelectionChange={setSelected} > <MenuItem id="left"> Left </MenuItem> <MenuItem id="center"> Center </MenuItem> <MenuItem id="right"> Right </MenuItem> </MyMenuButton> <p> Current selection (controlled): {' '} {[...selected] .join(', ')} </p> </> ); } Align Current selection (controlled): center Show CSS .react-aria-MenuItem { &[data-selection-mode] { padding-left: 24px; &::before { position: absolute; left: 4px; font-weight: 600; } &[data-selection-mode=multiple][data-selected]::before { content: '✓'; content: '✓' / ''; alt: ' '; position: absolute; left: 4px; font-weight: 600; } &[data-selection-mode=single][data-selected]::before { content: '●'; content: '●' / ''; transform: scale(0.7) } } } .react-aria-MenuItem { &[data-selection-mode] { padding-left: 24px; &::before { position: absolute; left: 4px; font-weight: 600; } &[data-selection-mode=multiple][data-selected]::before { content: '✓'; content: '✓' / ''; alt: ' '; position: absolute; left: 4px; font-weight: 600; } &[data-selection-mode=single][data-selected]::before { content: '●'; content: '●' / ''; transform: scale(0.7) } } } .react-aria-MenuItem { &[data-selection-mode] { padding-left: 24px; &::before { position: absolute; left: 4px; font-weight: 600; } &[data-selection-mode=multiple][data-selected]::before { content: '✓'; content: '✓' / ''; alt: ' '; position: absolute; left: 4px; font-weight: 600; } &[data-selection-mode=single][data-selected]::before { content: '●'; content: '●' / ''; transform: scale(0.7) } } } ### Multiple# import type {Selection} from 'react-aria-components'; function Example() { let [selected, setSelected] = React.useState<Selection>( new Set(['sidebar', 'console']) ); return ( <> <MyMenuButton label="View" selectionMode="multiple" selectedKeys={selected} onSelectionChange={setSelected} > <MenuItem id="sidebar">Sidebar</MenuItem> <MenuItem id="searchbar">Searchbar</MenuItem> <MenuItem id="tools">Tools</MenuItem> <MenuItem id="console">Console</MenuItem> </MyMenuButton> <p> Current selection (controlled):{' '} {selected === 'all' ? 'all' : [...selected].join(', ')} </p> </> ); } import type {Selection} from 'react-aria-components'; function Example() { let [selected, setSelected] = React.useState<Selection>( new Set(['sidebar', 'console']) ); return ( <> <MyMenuButton label="View" selectionMode="multiple" selectedKeys={selected} onSelectionChange={setSelected} > <MenuItem id="sidebar">Sidebar</MenuItem> <MenuItem id="searchbar">Searchbar</MenuItem> <MenuItem id="tools">Tools</MenuItem> <MenuItem id="console">Console</MenuItem> </MyMenuButton> <p> Current selection (controlled): {selected === 'all' ? 'all' : [...selected].join(', ')} </p> </> ); } import type {Selection} from 'react-aria-components'; function Example() { let [ selected, setSelected ] = React.useState< Selection >( new Set([ 'sidebar', 'console' ]) ); return ( <> <MyMenuButton label="View" selectionMode="multiple" selectedKeys={selected} onSelectionChange={setSelected} > <MenuItem id="sidebar"> Sidebar </MenuItem> <MenuItem id="searchbar"> Searchbar </MenuItem> <MenuItem id="tools"> Tools </MenuItem> <MenuItem id="console"> Console </MenuItem> </MyMenuButton> <p> Current selection (controlled): {' '} {selected === 'all' ? 'all' : [...selected] .join(', ')} </p> </> ); } View Current selection (controlled): sidebar, console ## Links# * * * By default, interacting with an item in a Menu triggers `onAction` and optionally `onSelectionChange` depending on the `selectionMode`. Alternatively, items may be links to another page or website. This can be achieved by passing the `href` prop to the `<MenuItem>` component. Link items in a menu are not selectable. <MyMenuButton label="Links"> <MenuItem href="https://adobe.com/" target="_blank">Adobe</MenuItem> <MenuItem href="https://apple.com/" target="_blank">Apple</MenuItem> <MenuItem href="https://google.com/" target="_blank">Google</MenuItem> <MenuItem href="https://microsoft.com/" target="_blank">Microsoft</MenuItem> </MyMenuButton> <MyMenuButton label="Links"> <MenuItem href="https://adobe.com/" target="_blank"> Adobe </MenuItem> <MenuItem href="https://apple.com/" target="_blank"> Apple </MenuItem> <MenuItem href="https://google.com/" target="_blank"> Google </MenuItem> <MenuItem href="https://microsoft.com/" target="_blank"> Microsoft </MenuItem> </MyMenuButton> <MyMenuButton label="Links"> <MenuItem href="https://adobe.com/" target="_blank" > Adobe </MenuItem> <MenuItem href="https://apple.com/" target="_blank" > Apple </MenuItem> <MenuItem href="https://google.com/" target="_blank" > Google </MenuItem> <MenuItem href="https://microsoft.com/" target="_blank" > Microsoft </MenuItem> </MyMenuButton> Links ### Client side routing# The `<MenuItem>` component works with frameworks and client side routers like Next.js and React Router. As with other React Aria components that support links, this works via the `RouterProvider` component at the root of your app. See the client side routing guide to learn how to set this up. ## Sections# * * * Menu supports sections with headings in order to group items. Sections can be used by wrapping groups of MenuItems in a `MenuSection` component. A `<Header>` element may also be included to label the section. ### Static items# import {MenuSection, Header} from 'react-aria-components'; <MyMenuButton label="Actions"> <MenuSection> <Header>Styles</Header> <MenuItem>Bold</MenuItem> <MenuItem>Underline</MenuItem> </MenuSection> <MenuSection> <Header>Align</Header> <MenuItem>Left</MenuItem> <MenuItem>Middle</MenuItem> <MenuItem>Right</MenuItem> </MenuSection> </MyMenuButton> import {MenuSection, Header} from 'react-aria-components'; <MyMenuButton label="Actions"> <MenuSection> <Header>Styles</Header> <MenuItem>Bold</MenuItem> <MenuItem>Underline</MenuItem> </MenuSection> <MenuSection> <Header>Align</Header> <MenuItem>Left</MenuItem> <MenuItem>Middle</MenuItem> <MenuItem>Right</MenuItem> </MenuSection> </MyMenuButton> import { Header, MenuSection } from 'react-aria-components'; <MyMenuButton label="Actions"> <MenuSection> <Header> Styles </Header> <MenuItem> Bold </MenuItem> <MenuItem> Underline </MenuItem> </MenuSection> <MenuSection> <Header> Align </Header> <MenuItem> Left </MenuItem> <MenuItem> Middle </MenuItem> <MenuItem> Right </MenuItem> </MenuSection> </MyMenuButton> Actions Show CSS .react-aria-Menu { .react-aria-MenuSection:not(:first-child) { margin-top: 12px; } .react-aria-Header { font-size: 1.143rem; font-weight: bold; padding: 0 0.714rem; } } .react-aria-Menu { .react-aria-MenuSection:not(:first-child) { margin-top: 12px; } .react-aria-Header { font-size: 1.143rem; font-weight: bold; padding: 0 0.714rem; } } .react-aria-Menu { .react-aria-MenuSection:not(:first-child) { margin-top: 12px; } .react-aria-Header { font-size: 1.143rem; font-weight: bold; padding: 0 0.714rem; } } ### Dynamic items# The above example shows sections with static items. Sections can also be populated from a hierarchical data structure. Similarly to the props on Menu, `<MenuSection>` takes an array of data using the `items` prop. If the section also has a header, the `Collection` component can be used to render the child items. import type {Selection} from 'react-aria-components'; import {Collection} from 'react-aria-components'; function Example() { let [selected, setSelected] = React.useState<Selection>(new Set([1,3])); let openWindows = [ { name: 'Left Panel', id: 'left', children: [ {id: 1, name: 'Final Copy (1)'} ] }, { name: 'Right Panel', id: 'right', children: [ {id: 2, name: 'index.ts'}, {id: 3, name: 'package.json'}, {id: 4, name: 'license.txt'} ] } ]; return ( <MyMenuButton label="Window" items={openWindows} selectionMode="multiple" selectedKeys={selected} onSelectionChange={setSelected}> {section => ( <MenuSection> <Header>{section.name}</Header> <Collection items={section.children}> {item => <MenuItem>{item.name}</MenuItem>} </Collection> </MenuSection> )} </MyMenuButton> ); } import type {Selection} from 'react-aria-components'; import {Collection} from 'react-aria-components'; function Example() { let [selected, setSelected] = React.useState<Selection>( new Set([1, 3]) ); let openWindows = [ { name: 'Left Panel', id: 'left', children: [ { id: 1, name: 'Final Copy (1)' } ] }, { name: 'Right Panel', id: 'right', children: [ { id: 2, name: 'index.ts' }, { id: 3, name: 'package.json' }, { id: 4, name: 'license.txt' } ] } ]; return ( <MyMenuButton label="Window" items={openWindows} selectionMode="multiple" selectedKeys={selected} onSelectionChange={setSelected} > {(section) => ( <MenuSection> <Header>{section.name}</Header> <Collection items={section.children}> {(item) => <MenuItem>{item.name}</MenuItem>} </Collection> </MenuSection> )} </MyMenuButton> ); } import type {Selection} from 'react-aria-components'; import {Collection} from 'react-aria-components'; function Example() { let [ selected, setSelected ] = React.useState< Selection >(new Set([1, 3])); let openWindows = [ { name: 'Left Panel', id: 'left', children: [ { id: 1, name: 'Final Copy (1)' } ] }, { name: 'Right Panel', id: 'right', children: [ { id: 2, name: 'index.ts' }, { id: 3, name: 'package.json' }, { id: 4, name: 'license.txt' } ] } ]; return ( <MyMenuButton label="Window" items={openWindows} selectionMode="multiple" selectedKeys={selected} onSelectionChange={setSelected} > {(section) => ( <MenuSection> <Header> {section .name} </Header> <Collection items={section .children} > {(item) => ( <MenuItem> {item .name} </MenuItem> )} </Collection> </MenuSection> )} </MyMenuButton> ); } Window ### Separators# Separators may be added between menu items or sections in order to create non-labeled groupings. import {Separator} from 'react-aria-components'; <MyMenuButton label="Actions"> <MenuItem>New…</MenuItem> <MenuItem>Open…</MenuItem> <Separator /> <MenuItem>Save</MenuItem> <MenuItem>Save as…</MenuItem> <MenuItem>Rename…</MenuItem> <Separator /> <MenuItem>Page setup…</MenuItem> <MenuItem>Print…</MenuItem> </MyMenuButton> import {Separator} from 'react-aria-components'; <MyMenuButton label="Actions"> <MenuItem>New…</MenuItem> <MenuItem>Open…</MenuItem> <Separator /> <MenuItem>Save</MenuItem> <MenuItem>Save as…</MenuItem> <MenuItem>Rename…</MenuItem> <Separator /> <MenuItem>Page setup…</MenuItem> <MenuItem>Print…</MenuItem> </MyMenuButton> import {Separator} from 'react-aria-components'; <MyMenuButton label="Actions"> <MenuItem> New… </MenuItem> <MenuItem> Open… </MenuItem> <Separator /> <MenuItem> Save </MenuItem> <MenuItem> Save as… </MenuItem> <MenuItem> Rename… </MenuItem> <Separator /> <MenuItem> Page setup… </MenuItem> <MenuItem> Print… </MenuItem> </MyMenuButton> Actions Show CSS .react-aria-Menu { .react-aria-Separator { height: 1px; background: var(--border-color); margin: 2px 4px; } } .react-aria-Menu { .react-aria-Separator { height: 1px; background: var(--border-color); margin: 2px 4px; } } .react-aria-Menu { .react-aria-Separator { height: 1px; background: var(--border-color); margin: 2px 4px; } } ### Section-level selection# Each section in a menu may have independent selection states. This works the same way as described above for the entire menu, but operates at the section level instead. function Example() { let [style, setStyle] = React.useState<Selection>(new Set(['bold'])); let [align, setAlign] = React.useState<Selection>(new Set(['left'])); return ( <MyMenuButton label="Edit"> <MenuSection> <Header>Actions</Header> <MenuItem>Cut</MenuItem> <MenuItem>Copy</MenuItem> <MenuItem>Paste</MenuItem> </MenuSection> <MenuSection selectionMode="multiple" selectedKeys={style} onSelectionChange={setStyle} > <Header>Text style</Header> <MenuItem id="bold">Bold</MenuItem> <MenuItem id="italic">Italic</MenuItem> <MenuItem id="underline">Underline</MenuItem> </MenuSection> <MenuSection selectionMode="single" selectedKeys={align} onSelectionChange={setAlign} > <Header>Text alignment</Header> <MenuItem id="left">Left</MenuItem> <MenuItem id="center">Center</MenuItem> <MenuItem id="right">Right</MenuItem> </MenuSection> </MyMenuButton> ); } function Example() { let [style, setStyle] = React.useState<Selection>( new Set(['bold']) ); let [align, setAlign] = React.useState<Selection>( new Set(['left']) ); return ( <MyMenuButton label="Edit"> <MenuSection> <Header>Actions</Header> <MenuItem>Cut</MenuItem> <MenuItem>Copy</MenuItem> <MenuItem>Paste</MenuItem> </MenuSection> <MenuSection selectionMode="multiple" selectedKeys={style} onSelectionChange={setStyle} > <Header>Text style</Header> <MenuItem id="bold">Bold</MenuItem> <MenuItem id="italic">Italic</MenuItem> <MenuItem id="underline">Underline</MenuItem> </MenuSection> <MenuSection selectionMode="single" selectedKeys={align} onSelectionChange={setAlign} > <Header>Text alignment</Header> <MenuItem id="left">Left</MenuItem> <MenuItem id="center">Center</MenuItem> <MenuItem id="right">Right</MenuItem> </MenuSection> </MyMenuButton> ); } function Example() { let [style, setStyle] = React.useState< Selection >(new Set(['bold'])); let [align, setAlign] = React.useState< Selection >(new Set(['left'])); return ( <MyMenuButton label="Edit"> <MenuSection> <Header> Actions </Header> <MenuItem> Cut </MenuItem> <MenuItem> Copy </MenuItem> <MenuItem> Paste </MenuItem> </MenuSection> <MenuSection selectionMode="multiple" selectedKeys={style} onSelectionChange={setStyle} > <Header> Text style </Header> <MenuItem id="bold"> Bold </MenuItem> <MenuItem id="italic"> Italic </MenuItem> <MenuItem id="underline"> Underline </MenuItem> </MenuSection> <MenuSection selectionMode="single" selectedKeys={align} onSelectionChange={setAlign} > <Header> Text alignment </Header> <MenuItem id="left"> Left </MenuItem> <MenuItem id="center"> Center </MenuItem> <MenuItem id="right"> Right </MenuItem> </MenuSection> </MyMenuButton> ); } Edit ### Accessibility# Sections without a `<Header>` must provide an `aria-label` for accessibility. ## Text slots# * * * By default, items in a `ListBox` are labeled by their text contents for accessibility. MenuItems also support the "label" and "description" slots to separate primary and secondary content, which improves screen reader announcements and can also be used for styling purposes. The `<Keyboard>` component can also be used to display a keyboard shortcut. import {Text, Keyboard} from 'react-aria-components'; <MyMenuButton label="Actions"> <MenuItem textValue="Copy"> <Text slot="label">Copy</Text> <Text slot="description">Copy the selected text</Text> <Keyboard>⌘C</Keyboard> </MenuItem> <MenuItem textValue="Cut"> <Text slot="label">Cut</Text> <Text slot="description">Cut the selected text</Text> <Keyboard>⌘X</Keyboard> </MenuItem> <MenuItem textValue="Paste"> <Text slot="label">Paste</Text> <Text slot="description">Paste the copied text</Text> <Keyboard>⌘V</Keyboard> </MenuItem> </MyMenuButton> import {Text, Keyboard} from 'react-aria-components'; <MyMenuButton label="Actions"> <MenuItem textValue="Copy"> <Text slot="label">Copy</Text> <Text slot="description">Copy the selected text</Text> <Keyboard>⌘C</Keyboard> </MenuItem> <MenuItem textValue="Cut"> <Text slot="label">Cut</Text> <Text slot="description">Cut the selected text</Text> <Keyboard>⌘X</Keyboard> </MenuItem> <MenuItem textValue="Paste"> <Text slot="label">Paste</Text> <Text slot="description">Paste the copied text</Text> <Keyboard>⌘V</Keyboard> </MenuItem> </MyMenuButton> import { Keyboard, Text } from 'react-aria-components'; <MyMenuButton label="Actions"> <MenuItem textValue="Copy"> <Text slot="label"> Copy </Text> <Text slot="description"> Copy the selected text </Text> <Keyboard> ⌘C </Keyboard> </MenuItem> <MenuItem textValue="Cut"> <Text slot="label"> Cut </Text> <Text slot="description"> Cut the selected text </Text> <Keyboard> ⌘X </Keyboard> </MenuItem> <MenuItem textValue="Paste"> <Text slot="label"> Paste </Text> <Text slot="description"> Paste the copied text </Text> <Keyboard> ⌘V </Keyboard> </MenuItem> </MyMenuButton> Actions Show CSS .react-aria-MenuItem { [slot=label] { font-weight: bold; grid-area: label; } [slot=description] { font-size: small; grid-area: desc; } kbd { grid-area: kbd; font-family: monospace; text-align: end; } } .react-aria-MenuItem { [slot=label] { font-weight: bold; grid-area: label; } [slot=description] { font-size: small; grid-area: desc; } kbd { grid-area: kbd; font-family: monospace; text-align: end; } } .react-aria-MenuItem { [slot=label] { font-weight: bold; grid-area: label; } [slot=description] { font-size: small; grid-area: desc; } kbd { grid-area: kbd; font-family: monospace; text-align: end; } } ## Long press# * * * By default, MenuTrigger opens by pressing the trigger element or activating it via the Space or Enter keys. However, there may be cases in which your trigger element should perform a separate default action on press, and should only display the Menu when long pressed. This behavior can be changed by providing `"longPress"` to the `trigger` prop. With this prop, the Menu will only be opened upon pressing and holding the trigger element or by using the Option (Alt on Windows) + Down Arrow/Up Arrow keys while focusing the trigger element. <MenuTrigger trigger="longPress"> <Button onPress={() => alert('crop')}>Crop</Button> <Popover> <Menu> <MenuItem>Rotate</MenuItem> <MenuItem>Slice</MenuItem> <MenuItem>Clone stamp</MenuItem> </Menu> </Popover> </MenuTrigger> <MenuTrigger trigger="longPress"> <Button onPress={() => alert('crop')}>Crop</Button> <Popover> <Menu> <MenuItem>Rotate</MenuItem> <MenuItem>Slice</MenuItem> <MenuItem>Clone stamp</MenuItem> </Menu> </Popover> </MenuTrigger> <MenuTrigger trigger="longPress"> <Button onPress={() => alert('crop')} > Crop </Button> <Popover> <Menu> <MenuItem> Rotate </MenuItem> <MenuItem> Slice </MenuItem> <MenuItem> Clone stamp </MenuItem> </Menu> </Popover> </MenuTrigger> Crop ## Disabled items# * * * A `MenuItem` can be disabled with the `isDisabled` prop. Disabled items are not focusable or keyboard navigable, and do not trigger `onAction` or `onSelectionChange`. <MyMenuButton label="Actions"> <MenuItem>Copy</MenuItem> <MenuItem>Cut</MenuItem> <MenuItem isDisabled>Paste</MenuItem></MyMenuButton> <MyMenuButton label="Actions"> <MenuItem>Copy</MenuItem> <MenuItem>Cut</MenuItem> <MenuItem isDisabled>Paste</MenuItem></MyMenuButton> <MyMenuButton label="Actions"> <MenuItem> Copy </MenuItem> <MenuItem> Cut </MenuItem> <MenuItem isDisabled> Paste </MenuItem></MyMenuButton> Actions Show CSS .react-aria-MenuItem { &[data-disabled] { color: var(--text-color-disabled); } } .react-aria-MenuItem { &[data-disabled] { color: var(--text-color-disabled); } } .react-aria-MenuItem { &[data-disabled] { color: var(--text-color-disabled); } } In dynamic collections, it may be more convenient to use the `disabledKeys` prop at the `Menu` level instead of `isDisabled` on individual items. Each key in this list corresponds with the `id` prop passed to the `MenuItem` component, or automatically derived from the values passed to the `items` prop (see the Collections for more details). An item is considered disabled if its id exists in `disabledKeys` or if it has `isDisabled`. function Example() { let items = [ {id: 1, name: 'New'}, {id: 2, name: 'Open'}, {id: 3, name: 'Close'}, {id: 4, name: 'Save'}, {id: 5, name: 'Duplicate'}, {id: 6, name: 'Rename'}, {id: 7, name: 'Move'} ]; return ( <MyMenuButton label="Actions" items={items} disabledKeys={[4, 6]} > {(item) => <MenuItem>{item.name}</MenuItem>} </MyMenuButton> ); } function Example() { let items = [ {id: 1, name: 'New'}, {id: 2, name: 'Open'}, {id: 3, name: 'Close'}, {id: 4, name: 'Save'}, {id: 5, name: 'Duplicate'}, {id: 6, name: 'Rename'}, {id: 7, name: 'Move'} ]; return ( <MyMenuButton label="Actions" items={items} disabledKeys={[4, 6]} > {(item) => <MenuItem>{item.name}</MenuItem>} </MyMenuButton> ); } function Example() { let items = [ { id: 1, name: 'New' }, { id: 2, name: 'Open' }, { id: 3, name: 'Close' }, { id: 4, name: 'Save' }, { id: 5, name: 'Duplicate' }, { id: 6, name: 'Rename' }, { id: 7, name: 'Move' } ]; return ( <MyMenuButton label="Actions" items={items} disabledKeys={[ 4, 6 ]} > {(item) => ( <MenuItem> {item.name} </MenuItem> )} </MyMenuButton> ); } Actions ## Controlled open state# * * * The open state of the menu can be controlled via the `defaultOpen` and `isOpen` props. function Example() { let [open, setOpen] = React.useState(false); return ( <> <p>Menu is {open ? 'open' : 'closed'}</p> <MyMenuButton label="View" isOpen={open} onOpenChange={setOpen}> <MenuItem id="side">Side bar</MenuItem> <MenuItem id="options">Page options</MenuItem> <MenuItem id="edit">Edit Panel</MenuItem> </MyMenuButton> </> ); } function Example() { let [open, setOpen] = React.useState(false); return ( <> <p>Menu is {open ? 'open' : 'closed'}</p> <MyMenuButton label="View" isOpen={open} onOpenChange={setOpen}> <MenuItem id="side">Side bar</MenuItem> <MenuItem id="options">Page options</MenuItem> <MenuItem id="edit">Edit Panel</MenuItem> </MyMenuButton> </> ); } function Example() { let [open, setOpen] = React.useState( false ); return ( <> <p> Menu is {open ? 'open' : 'closed'} </p> <MyMenuButton label="View" isOpen={open} onOpenChange={setOpen} > <MenuItem id="side"> Side bar </MenuItem> <MenuItem id="options"> Page options </MenuItem> <MenuItem id="edit"> Edit Panel </MenuItem> </MyMenuButton> </> ); } Menu is closed View ## Submenus# * * * Submenus can be created by wrapping an item and a submenu in a `SubmenuTrigger`. The `SubmenuTrigger` accepts exactly two children: the first child should be the `MenuItem` which triggers opening of the submenu, and second child should be the `Popover` containing the submenu. ### Static# import {Menu, Popover, SubmenuTrigger} from 'react-aria-components'; <MyMenuButton label="Actions"> <MyItem>Cut</MyItem> <MyItem>Copy</MyItem> <MyItem>Delete</MyItem> <SubmenuTrigger> <MyItem>Share</MyItem> <Popover> <Menu> <MyItem>SMS</MyItem> <MyItem>X</MyItem> <SubmenuTrigger> <MyItem>Email</MyItem> <Popover> <Menu> <MyItem>Work</MyItem> <MyItem>Personal</MyItem> </Menu> </Popover> </SubmenuTrigger> </Menu> </Popover> </SubmenuTrigger> </MyMenuButton> import { Menu, Popover, SubmenuTrigger } from 'react-aria-components'; <MyMenuButton label="Actions"> <MyItem>Cut</MyItem> <MyItem>Copy</MyItem> <MyItem>Delete</MyItem> <SubmenuTrigger> <MyItem>Share</MyItem> <Popover> <Menu> <MyItem>SMS</MyItem> <MyItem>X</MyItem> <SubmenuTrigger> <MyItem>Email</MyItem> <Popover> <Menu> <MyItem>Work</MyItem> <MyItem>Personal</MyItem> </Menu> </Popover> </SubmenuTrigger> </Menu> </Popover> </SubmenuTrigger> </MyMenuButton> import { Menu, Popover, SubmenuTrigger } from 'react-aria-components'; <MyMenuButton label="Actions"> <MyItem>Cut</MyItem> <MyItem>Copy</MyItem> <MyItem> Delete </MyItem> <SubmenuTrigger> <MyItem> Share </MyItem> <Popover> <Menu> <MyItem> SMS </MyItem> <MyItem> X </MyItem> <SubmenuTrigger> <MyItem> Email </MyItem> <Popover> <Menu> <MyItem> Work </MyItem> <MyItem> Personal </MyItem> </Menu> </Popover> </SubmenuTrigger> </Menu> </Popover> </SubmenuTrigger> </MyMenuButton> Actions ### Dynamic# You can define a recursive function to render the nested menu items dynamically. import {Menu, Popover, SubmenuTrigger} from 'react-aria-components'; let items = [ {id: 'cut', name: 'Cut'}, {id: 'copy', name: 'Copy'}, {id: 'delete', name: 'Delete'}, {id: 'share', name: 'Share', children: [ {id: 'sms', name: 'SMS'}, {id: 'x', name: 'X'}, {id: 'email', name: 'Email', children: [ {id: 'work', name: 'Work'}, {id: 'personal', name: 'Personal'}, ]} ]} ]; <MyMenuButton label="Actions" items={items}> {function renderSubmenu(item) { if (item.children) { return ( <SubmenuTrigger> <MyItem key={item.name}>{item.name}</MyItem> <Popover> <Menu items={item.children}> {(item) => renderSubmenu(item)} </Menu> </Popover> </SubmenuTrigger> ); } else { return <MyItem key={item.name}>{item.name}</MyItem>; } }} </MyMenuButton> import { Menu, Popover, SubmenuTrigger } from 'react-aria-components'; let items = [ { id: 'cut', name: 'Cut' }, { id: 'copy', name: 'Copy' }, { id: 'delete', name: 'Delete' }, { id: 'share', name: 'Share', children: [ { id: 'sms', name: 'SMS' }, { id: 'x', name: 'X' }, { id: 'email', name: 'Email', children: [ { id: 'work', name: 'Work' }, { id: 'personal', name: 'Personal' } ] } ] } ]; <MyMenuButton label="Actions" items={items}> {function renderSubmenu(item) { if (item.children) { return ( <SubmenuTrigger> <MyItem key={item.name}>{item.name}</MyItem> <Popover> <Menu items={item.children}> {(item) => renderSubmenu(item)} </Menu> </Popover> </SubmenuTrigger> ); } else { return <MyItem key={item.name}>{item.name}</MyItem>; } }} </MyMenuButton> import { Menu, Popover, SubmenuTrigger } from 'react-aria-components'; let items = [ { id: 'cut', name: 'Cut' }, { id: 'copy', name: 'Copy' }, { id: 'delete', name: 'Delete' }, { id: 'share', name: 'Share', children: [ { id: 'sms', name: 'SMS' }, { id: 'x', name: 'X' }, { id: 'email', name: 'Email', children: [ { id: 'work', name: 'Work' }, { id: 'personal', name: 'Personal' } ] } ] } ]; <MyMenuButton label="Actions" items={items} > {function renderSubmenu( item ) { if ( item.children ) { return ( <SubmenuTrigger> <MyItem key={item .name} > {item.name} </MyItem> <Popover> <Menu items={item .children} > {( item ) => renderSubmenu( item )} </Menu> </Popover> </SubmenuTrigger> ); } else { return ( <MyItem key={item .name} > {item.name} </MyItem> ); } }} </MyMenuButton> Actions Show CSS .react-aria-Popover[data-trigger=SubmenuTrigger][data-placement="right"] { margin-left: -5px; } .react-aria-Popover[data-trigger=SubmenuTrigger][data-placement="left"] { margin-right: -5px; } .react-aria-Popover[data-trigger=SubmenuTrigger][data-placement="right"] { margin-left: -5px; } .react-aria-Popover[data-trigger=SubmenuTrigger][data-placement="left"] { margin-right: -5px; } .react-aria-Popover[data-trigger=SubmenuTrigger][data-placement="right"] { margin-left: -5px; } .react-aria-Popover[data-trigger=SubmenuTrigger][data-placement="left"] { margin-right: -5px; } ### Complex content# Submenu popovers can also include components other than menus. This example uses an Autocomplete to make the submenu searchable. import {Autocomplete, Menu, Popover, SubmenuTrigger, useFilter} from 'react-aria-components'; import {MySearchField} from './SearchField'; function Example() { let { contains } = useFilter({ sensitivity: 'base' }); return ( <MyMenuButton label="Actions"> <MyItem>Cut</MyItem> <MyItem>Copy</MyItem> <MyItem>Delete</MyItem> <SubmenuTrigger> <MyItem>Add tag...</MyItem> <Popover> <Autocomplete filter={contains}> <MySearchField label="Search tags" autoFocus /> <Menu> <MyItem>News</MyItem> <MyItem>Travel</MyItem> <MyItem>Shopping</MyItem> <MyItem>Business</MyItem> <MyItem>Entertainment</MyItem> <MyItem>Food</MyItem> <MyItem>Technology</MyItem> <MyItem>Health</MyItem> <MyItem>Science</MyItem> </Menu> </Autocomplete> </Popover> </SubmenuTrigger> </MyMenuButton> ); } import { Autocomplete, Menu, Popover, SubmenuTrigger, useFilter } from 'react-aria-components'; import {MySearchField} from './SearchField'; function Example() { let { contains } = useFilter({ sensitivity: 'base' }); return ( <MyMenuButton label="Actions"> <MyItem>Cut</MyItem> <MyItem>Copy</MyItem> <MyItem>Delete</MyItem> <SubmenuTrigger> <MyItem>Add tag...</MyItem> <Popover> <Autocomplete filter={contains}> <MySearchField label="Search tags" autoFocus /> <Menu> <MyItem>News</MyItem> <MyItem>Travel</MyItem> <MyItem>Shopping</MyItem> <MyItem>Business</MyItem> <MyItem>Entertainment</MyItem> <MyItem>Food</MyItem> <MyItem>Technology</MyItem> <MyItem>Health</MyItem> <MyItem>Science</MyItem> </Menu> </Autocomplete> </Popover> </SubmenuTrigger> </MyMenuButton> ); } import { Autocomplete, Menu, Popover, SubmenuTrigger, useFilter } from 'react-aria-components'; import {MySearchField} from './SearchField'; function Example() { let { contains } = useFilter({ sensitivity: 'base' }); return ( <MyMenuButton label="Actions"> <MyItem> Cut </MyItem> <MyItem> Copy </MyItem> <MyItem> Delete </MyItem> <SubmenuTrigger> <MyItem> Add tag... </MyItem> <Popover> <Autocomplete filter={contains} > <MySearchField label="Search tags" autoFocus /> <Menu> <MyItem> News </MyItem> <MyItem> Travel </MyItem> <MyItem> Shopping </MyItem> <MyItem> Business </MyItem> <MyItem> Entertainment </MyItem> <MyItem> Food </MyItem> <MyItem> Technology </MyItem> <MyItem> Health </MyItem> <MyItem> Science </MyItem> </Menu> </Autocomplete> </Popover> </SubmenuTrigger> </MyMenuButton> ); } Actions ## Custom trigger# * * * `MenuTrigger` works out of the box with any pressable React Aria component (e.g. Button, Link, etc.). Custom trigger elements such as third party components and other DOM elements are also supported by wrapping them with the `<Pressable>` component, or using the usePress hook. import {Pressable} from 'react-aria-components'; <MenuTrigger> <Pressable> <span role="button">Custom trigger</span> </Pressable> <Popover> <Menu> <MenuItem>Open</MenuItem> <MenuItem>Rename…</MenuItem> <MenuItem>Duplicate</MenuItem> <MenuItem>Delete…</MenuItem> </Menu> </Popover> </MenuTrigger> import {Pressable} from 'react-aria-components'; <MenuTrigger> <Pressable> <span role="button">Custom trigger</span> </Pressable> <Popover> <Menu> <MenuItem>Open</MenuItem> <MenuItem>Rename…</MenuItem> <MenuItem>Duplicate</MenuItem> <MenuItem>Delete…</MenuItem> </Menu> </Popover> </MenuTrigger> import {Pressable} from 'react-aria-components'; <MenuTrigger> <Pressable> <span role="button"> Custom trigger </span> </Pressable> <Popover> <Menu> <MenuItem> Open </MenuItem> <MenuItem> Rename… </MenuItem> <MenuItem> Duplicate </MenuItem> <MenuItem> Delete… </MenuItem> </Menu> </Popover> </MenuTrigger> Custom trigger Note that any `<Pressable>` child must have an interactive ARIA role or use an appropriate semantic HTML element so that screen readers can announce the trigger. Trigger components must forward their `ref` and spread all props to a DOM element. const CustomTrigger = React.forwardRef((props, ref) => ( <button {...props} ref={ref} /> )); const CustomTrigger = React.forwardRef((props, ref) => ( <button {...props} ref={ref} /> )); const CustomTrigger = React.forwardRef(( props, ref ) => ( <button {...props} ref={ref} /> )); ## Props# * * * ### MenuTrigger# | Name | Type | Default | Description | | --- | --- | --- | --- | | `children` | `ReactNode` | — | | | `trigger` | ` MenuTriggerType ` | `'press'` | How the menu is triggered. | | `isOpen` | `boolean` | — | Whether the overlay is open by default (controlled). | | `defaultOpen` | `boolean` | — | Whether the overlay is open by default (uncontrolled). | Events | Name | Type | Description | | --- | --- | --- | | `onOpenChange` | `( (isOpen: boolean )) => void` | Handler that is called when the overlay's open state changes. | ### SubmenuTrigger# | Name | Type | Default | Description | | --- | --- | --- | --- | | `children` | `ReactElement[]` | — | The contents of the SubmenuTrigger. The first child should be an Item (the trigger) and the second child should be the Popover (for the submenu). | | `delay` | `number` | `200` | The delay time in milliseconds for the submenu to appear after hovering over the trigger. | ### Button# A `<Button>` accepts its contents as `children`. Other props such as `onPress` and `isDisabled` will be set by the `MenuTrigger`. Show props | Name | Type | Default | Description | | --- | --- | --- | --- | | `form` | `string` | — | The `<form>` element to associate the button with. The value of this attribute must be the id of a `<form>` in the same document. | | `formAction` | `string` | — | The URL that processes the information submitted by the button. Overrides the action attribute of the button's form owner. | | `formEncType` | `string` | — | Indicates how to encode the form data that is submitted. | | `formMethod` | `string` | — | Indicates the HTTP method used to submit the form. | | `formNoValidate` | `boolean` | — | Indicates that the form is not to be validated when it is submitted. | | `formTarget` | `string` | — | Overrides the target attribute of the button's form owner. | | `name` | `string` | — | Submitted as a pair with the button's value as part of the form data. | | `value` | `string` | — | The value associated with the button's name when it's submitted with the form data. | | `isPending` | `boolean` | — | Whether the button is in a pending state. This disables press and hover events while retaining focusability, and announces the pending state to screen readers. | | `isDisabled` | `boolean` | — | Whether the button is disabled. | | `autoFocus` | `boolean` | — | Whether the element should receive focus on render. | | `type` | `'button' | 'submit' | 'reset'` | `'button'` | The behavior of the button when used in an HTML form. | | `children` | `ReactNode | ( (values: ButtonRenderProps & & { defaultChildren: ReactNode | | undefined } )) => ReactNode` | — | The children of the component. A function may be provided to alter the children based on component state. | | `className` | `string | ( (values: ButtonRenderProps & & { defaultClassName: string | | undefined } )) => string` | — | The CSS className for the element. A function may be provided to compute the class based on component state. | | `style` | `CSSProperties | ( (values: ButtonRenderProps & & { defaultStyle: CSSProperties } )) => CSSProperties | undefined` | — | The inline style for the element. A function may be provided to compute the style based on component state. | Events | Name | Type | Description | | --- | --- | --- | | `onPress` | `( (e: PressEvent )) => void` | Handler that is called when the press is released over the target. | | `onPressStart` | `( (e: PressEvent )) => void` | Handler that is called when a press interaction starts. | | `onPressEnd` | `( (e: PressEvent )) => void` | Handler that is called when a press interaction ends, either over the target or when the pointer leaves the target. | | `onPressChange` | `( (isPressed: boolean )) => void` | Handler that is called when the press state changes. | | `onPressUp` | `( (e: PressEvent )) => void` | Handler that is called when a press is released over the target, regardless of whether it started on the target or not. | | `onClick` | `( (e: MouseEvent<FocusableElement> )) => void` | **Not recommended – use `onPress` instead.** `onClick` is an alias for `onPress` provided for compatibility with other libraries. `onPress` provides additional event details for non-mouse interactions. | | `onFocus` | `( (e: FocusEvent<Target> )) => void` | Handler that is called when the element receives focus. | | `onBlur` | `( (e: FocusEvent<Target> )) => void` | Handler that is called when the element loses focus. | | `onFocusChange` | `( (isFocused: boolean )) => void` | Handler that is called when the element's focus status changes. | | `onKeyDown` | `( (e: KeyboardEvent )) => void` | Handler that is called when a key is pressed. | | `onKeyUp` | `( (e: KeyboardEvent )) => void` | Handler that is called when a key is released. | | `onHoverStart` | `( (e: HoverEvent )) => void` | Handler that is called when a hover interaction starts. | | `onHoverEnd` | `( (e: HoverEvent )) => void` | Handler that is called when a hover interaction ends. | | `onHoverChange` | `( (isHovering: boolean )) => void` | Handler that is called when the hover state changes. | Layout | Name | Type | Description | | --- | --- | --- | | `slot` | `string | null` | A slot name for the component. Slots allow the component to receive props from a parent component. An explicit `null` value indicates that the local props completely override all props received from a parent. | Accessibility | Name | Type | Description | | --- | --- | --- | | `id` | `string` | The element's unique identifier. See MDN. | | `excludeFromTabOrder` | `boolean` | Whether to exclude the element from the sequential tab order. If true, the element will not be focusable via the keyboard by tabbing. This should be avoided except in rare scenarios where an alternative means of accessing the element or its functionality via the keyboard is available. | | `preventFocusOnPress` | `boolean` | Whether to prevent focus from moving to the button when pressing it. Caution, this can make the button inaccessible and should only be used when alternative keyboard interaction is provided, such as ComboBox's MenuTrigger or a NumberField's increment/decrement control. | | `aria-expanded` | `boolean | 'true' | 'false'` | Indicates whether the element, or another grouping element it controls, is currently expanded or collapsed. | | `aria-haspopup` | `boolean | 'menu' | 'listbox' | 'tree' | 'grid' | 'dialog' | 'true' | 'false'` | Indicates the availability and type of interactive popup element, such as menu or dialog, that can be triggered by an element. | | `aria-controls` | `string` | Identifies the element (or elements) whose contents or presence are controlled by the current element. | | `aria-pressed` | `boolean | 'true' | 'false' | 'mixed'` | Indicates the current "pressed" state of toggle buttons. | | `aria-current` | `boolean | 'true' | 'false' | 'page' | 'step' | 'location' | 'date' | 'time'` | Indicates whether this element represents the current item within a container or set of related elements. | | `aria-label` | `string` | Defines a string value that labels the current element. | | `aria-labelledby` | `string` | Identifies the element (or elements) that labels the current element. | | `aria-describedby` | `string` | Identifies the element (or elements) that describes the object. | | `aria-details` | `string` | Identifies the element (or elements) that provide a detailed, extended description for the object. | ### Popover# A `<Popover>` is a container to hold the `<Menu>`. By default, it has a `placement` of `bottom start` within a `<MenuTrigger>`, but this and other positioning properties may be customized. Show props | Name | Type | Default | Description | | --- | --- | --- | --- | | `trigger` | `string` | — | The name of the component that triggered the popover. This is reflected on the element as the `data-trigger` attribute, and can be used to provide specific styles for the popover depending on which element triggered it. | | `triggerRef` | ` RefObject <Element | null>` | — | The ref for the element which the popover positions itself with respect to. When used within a trigger component such as DialogTrigger, MenuTrigger, Select, etc., this is set automatically. It is only required when used standalone. | | `isEntering` | `boolean` | — | Whether the popover is currently performing an entry animation. | | `isExiting` | `boolean` | — | Whether the popover is currently performing an exit animation. | | `offset` | `number` | `8` | The additional offset applied along the main axis between the element and its anchor element. | | `placement` | ` Placement ` | `'bottom'` | The placement of the element with respect to its anchor element. | | `containerPadding` | `number` | `12` | The placement padding that should be applied between the element and its surrounding container. | | `crossOffset` | `number` | `0` | The additional offset applied along the cross axis between the element and its anchor element. | | `shouldFlip` | `boolean` | `true` | Whether the element should flip its orientation (e.g. top to bottom or left to right) when there is insufficient room for it to render completely. | | `isNonModal` | `boolean` | — | Whether the popover is non-modal, i.e. elements outside the popover may be interacted with by assistive technologies. Most popovers should not use this option as it may negatively impact the screen reader experience. Only use with components such as combobox, which are designed to handle this situation carefully. | | `isKeyboardDismissDisabled` | `boolean` | `false` | Whether pressing the escape key to close the popover should be disabled. Most popovers should not use this option. When set to true, an alternative way to close the popover with a keyboard must be provided. | | `shouldCloseOnInteractOutside` | `( (element: Element )) => boolean` | — | When user interacts with the argument element outside of the popover ref, return true if onClose should be called. This gives you a chance to filter out interaction with elements that should not dismiss the popover. By default, onClose will always be called on interaction outside the popover ref. | | `boundaryElement` | `Element` | `document.body` | Element that that serves as the positioning boundary. | | `scrollRef` | ` RefObject <Element | null>` | `overlayRef` | A ref for the scrollable region within the overlay. | | `shouldUpdatePosition` | `boolean` | `true` | Whether the overlay should update its position automatically. | | `arrowBoundaryOffset` | `number` | `0` | The minimum distance the arrow's edge should be from the edge of the overlay element. | | `isOpen` | `boolean` | — | Whether the overlay is open by default (controlled). | | `defaultOpen` | `boolean` | — | Whether the overlay is open by default (uncontrolled). | | `children` | `ReactNode | ( (values: PopoverRenderProps & & { defaultChildren: ReactNode | | undefined } )) => ReactNode` | — | The children of the component. A function may be provided to alter the children based on component state. | | `className` | `string | ( (values: PopoverRenderProps & & { defaultClassName: string | | undefined } )) => string` | — | The CSS className for the element. A function may be provided to compute the class based on component state. | | `style` | `CSSProperties | ( (values: PopoverRenderProps & & { defaultStyle: CSSProperties } )) => CSSProperties | undefined` | — | The inline style for the element. A function may be provided to compute the style based on component state. | Events | Name | Type | Description | | --- | --- | --- | | `onOpenChange` | `( (isOpen: boolean )) => void` | Handler that is called when the overlay's open state changes. | Layout | Name | Type | Description | | --- | --- | --- | | `slot` | `string | null` | A slot name for the component. Slots allow the component to receive props from a parent component. An explicit `null` value indicates that the local props completely override all props received from a parent. | Sizing | Name | Type | Description | | --- | --- | --- | | `maxHeight` | `number` | The maxHeight specified for the overlay element. By default, it will take all space up to the current viewport height. | Accessibility | Name | Type | Description | | --- | --- | --- | | `aria-label` | `string` | Defines a string value that labels the current element. | | `aria-labelledby` | `string` | Identifies the element (or elements) that labels the current element. | | `aria-describedby` | `string` | Identifies the element (or elements) that describes the object. | | `aria-details` | `string` | Identifies the element (or elements) that provide a detailed, extended description for the object. | ### Menu# | Name | Type | Default | Description | | --- | --- | --- | --- | | `renderEmptyState` | `() => ReactNode` | — | Provides content to display when there are no items in the list. | | `escapeKeyBehavior` | `'clearSelection' | 'none'` | `'clearSelection'` | Whether pressing the escape key should clear selection in the menu or not. Most experiences should not modify this option as it eliminates a keyboard user's ability to easily clear selection. Only use if the escape key is being handled externally or should not trigger selection clearing contextually. | | `autoFocus` | `boolean | FocusStrategy ` | — | Where the focus should be set. | | `shouldFocusWrap` | `boolean` | — | Whether keyboard navigation is circular. | | `items` | `Iterable<T>` | — | Item objects in the collection. | | `disabledKeys` | `Iterable<Key>` | — | The item keys that are disabled. These items cannot be selected, focused, or otherwise interacted with. | | `selectionMode` | ` SelectionMode ` | — | The type of selection that is allowed in the collection. | | `disallowEmptySelection` | `boolean` | — | Whether the collection allows empty selection. | | `selectedKeys` | `'all' | Iterable<Key>` | — | The currently selected keys in the collection (controlled). | | `defaultSelectedKeys` | `'all' | Iterable<Key>` | — | The initial selected keys in the collection (uncontrolled). | | `children` | `ReactNode | ( (item: object )) => ReactNode` | — | The contents of the collection. | | `dependencies` | `ReadonlyArray<any>` | — | Values that should invalidate the item cache when using dynamic collections. | | `className` | `string | ( (values: MenuRenderProps & & { defaultClassName: string | | undefined } )) => string` | — | The CSS className for the element. A function may be provided to compute the class based on component state. | | `style` | `CSSProperties | ( (values: MenuRenderProps & & { defaultStyle: CSSProperties } )) => CSSProperties | undefined` | — | The inline style for the element. A function may be provided to compute the style based on component state. | Events | Name | Type | Description | | --- | --- | --- | | `onAction` | `( (key: Key )) => void` | Handler that is called when an item is selected. | | `onClose` | `() => void` | Handler that is called when the menu should close after selecting an item. | | `onSelectionChange` | `( (keys: Selection )) => void` | Handler that is called when the selection changes. | | `onScroll` | `( (e: UIEvent<Element> )) => void` | Handler that is called when a user scrolls. See MDN. | Layout | Name | Type | Description | | --- | --- | --- | | `slot` | `string | null` | A slot name for the component. Slots allow the component to receive props from a parent component. An explicit `null` value indicates that the local props completely override all props received from a parent. | Accessibility | Name | Type | Description | | --- | --- | --- | | `id` | `string` | The element's unique identifier. See MDN. | | `aria-label` | `string` | Defines a string value that labels the current element. | | `aria-labelledby` | `string` | Identifies the element (or elements) that labels the current element. | | `aria-describedby` | `string` | Identifies the element (or elements) that describes the object. | | `aria-details` | `string` | Identifies the element (or elements) that provide a detailed, extended description for the object. | ### MenuSection# A `<MenuSection>` defines the child items for a section within a `<Menu>`. It may also contain an optional `<Header>` element. If there is no header, then an `aria-label` must be provided to identify the section to assistive technologies. Show props | Name | Type | Description | | --- | --- | --- | | `id` | `Key` | The unique id of the section. | | `value` | `object` | The object value that this section represents. When using dynamic collections, this is set automatically. | | `children` | `ReactNode | ( (item: object )) => ReactElement` | Static child items or a function to render children. | | `dependencies` | `ReadonlyArray<any>` | Values that should invalidate the item cache when using dynamic collections. | | `items` | `Iterable<object>` | Item objects in the section. | | `className` | `string` | The CSS className for the element. | | `style` | `CSSProperties` | The inline style for the element. | | `selectionMode` | ` SelectionMode ` | The type of selection that is allowed in the collection. | | `disallowEmptySelection` | `boolean` | Whether the collection allows empty selection. | | `selectedKeys` | `'all' | Iterable<Key>` | The currently selected keys in the collection (controlled). | | `defaultSelectedKeys` | `'all' | Iterable<Key>` | The initial selected keys in the collection (uncontrolled). | | `disabledKeys` | `Iterable<Key>` | The currently disabled keys in the collection (controlled). | Events | Name | Type | Description | | --- | --- | --- | | `onSelectionChange` | `( (keys: Selection )) => void` | Handler that is called when the selection changes. | Accessibility | Name | Type | Description | | --- | --- | --- | | `aria-label` | `string` | An accessibility label for the section. | ### Header# A `<Header>` defines the title for a `<MenuSection>`. It accepts all DOM attributes. ### MenuItem# A `<MenuItem>` defines a single item within a `<Menu>`. If the `children` are not plain text, then the `textValue` prop must also be set to a plain text representation, which will be used for autocomplete in the Menu. Show props | Name | Type | Description | | --- | --- | --- | | `id` | `Key` | The unique id of the item. | | `value` | `object` | The object value that this item represents. When using dynamic collections, this is set automatically. | | `textValue` | `string` | A string representation of the item's contents, used for features like typeahead. | | `isDisabled` | `boolean` | Whether the item is disabled. | | `children` | `ReactNode | ( (values: MenuItemRenderProps & & { defaultChildren: ReactNode | | undefined } )) => ReactNode` | The children of the component. A function may be provided to alter the children based on component state. | | `className` | `string | ( (values: MenuItemRenderProps & & { defaultClassName: string | | undefined } )) => string` | The CSS className for the element. A function may be provided to compute the class based on component state. | | `style` | `CSSProperties | ( (values: MenuItemRenderProps & & { defaultStyle: CSSProperties } )) => CSSProperties | undefined` | The inline style for the element. A function may be provided to compute the style based on component state. | | `href` | ` Href ` | A URL to link to. See MDN. | | `hrefLang` | `string` | Hints at the human language of the linked URL. SeeMDN. | | `target` | `HTMLAttributeAnchorTarget` | The target window for the link. See MDN. | | `rel` | `string` | The relationship between the linked resource and the current page. See MDN. | | `download` | `boolean | string` | Causes the browser to download the linked URL. A string may be provided to suggest a file name. See MDN. | | `ping` | `string` | A space-separated list of URLs to ping when the link is followed. See MDN. | | `referrerPolicy` | `HTMLAttributeReferrerPolicy` | How much of the referrer to send when following the link. See MDN. | | `routerOptions` | ` RouterOptions ` | Options for the configured client side router. | Events | Name | Type | Description | | --- | --- | --- | | `onAction` | `() => void` | Handler that is called when the item is selected. | | `onHoverStart` | `( (e: HoverEvent )) => void` | Handler that is called when a hover interaction starts. | | `onHoverEnd` | `( (e: HoverEvent )) => void` | Handler that is called when a hover interaction ends. | | `onHoverChange` | `( (isHovering: boolean )) => void` | Handler that is called when the hover state changes. | Accessibility | Name | Type | Description | | --- | --- | --- | | `aria-label` | `string` | An accessibility label for this item. | ### Separator# A `<Separator>` can be placed between menu items. Show props | Name | Type | Default | Description | | --- | --- | --- | --- | | `orientation` | ` Orientation ` | `'horizontal'` | The orientation of the separator. | | `elementType` | `string` | — | The HTML element type that will be used to render the separator. | | `className` | `string` | — | The CSS className for the element. | | `style` | `CSSProperties` | — | The inline style for the element. | Layout | Name | Type | Description | | --- | --- | --- | | `slot` | `string | null` | A slot name for the component. Slots allow the component to receive props from a parent component. An explicit `null` value indicates that the local props completely override all props received from a parent. | Accessibility | Name | Type | Description | | --- | --- | --- | | `id` | `string` | The element's unique identifier. See MDN. | | `aria-label` | `string` | Defines a string value that labels the current element. | | `aria-labelledby` | `string` | Identifies the element (or elements) that labels the current element. | | `aria-describedby` | `string` | Identifies the element (or elements) that describes the object. | | `aria-details` | `string` | Identifies the element (or elements) that provide a detailed, extended description for the object. | ## Styling# * * * React Aria components can be styled in many ways, including using CSS classes, inline styles, utility classes (e.g. Tailwind), CSS-in-JS (e.g. Styled Components), etc. By default, all components include a builtin `className` attribute which can be targeted using CSS selectors. These follow the `react-aria-ComponentName` naming convention. .react-aria-Menu { /* ... */ } .react-aria-Menu { /* ... */ } .react-aria-Menu { /* ... */ } A custom `className` can also be specified on any component. This overrides the default `className` provided by React Aria with your own. <Menu className="my-menu"> {/* ... */} </Menu> <Menu className="my-menu"> {/* ... */} </Menu> <Menu className="my-menu"> {/* ... */} </Menu> In addition, some components support multiple UI states (e.g. pressed, hovered, etc.). React Aria components expose states using data attributes, which you can target in CSS selectors. For example: .react-aria-MenuItem[data-selected] { /* ... */ } .react-aria-MenuItem[data-focused] { /* ... */ } .react-aria-MenuItem[data-selected] { /* ... */ } .react-aria-MenuItem[data-focused] { /* ... */ } .react-aria-MenuItem[data-selected] { /* ... */ } .react-aria-MenuItem[data-focused] { /* ... */ } The `className` and `style` props also accept functions which receive states for styling. This lets you dynamically determine the classes or styles to apply, which is useful when using utility CSS libraries like Tailwind. <MenuItem className={({ isSelected }) => isSelected ? 'bg-blue-400' : 'bg-gray-100'} > Item </MenuItem> <MenuItem className={({ isSelected }) => isSelected ? 'bg-blue-400' : 'bg-gray-100'} > Item </MenuItem> <MenuItem className={( { isSelected } ) => isSelected ? 'bg-blue-400' : 'bg-gray-100'} > Item </MenuItem> 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. <MenuItem> {({isSelected}) => ( <> {isSelected && <CheckmarkIcon />} Item </> )} </MenuItem> <MenuItem> {({isSelected}) => ( <> {isSelected && <CheckmarkIcon />} Item </> )} </MenuItem> <MenuItem> {( { isSelected } ) => ( <> {isSelected && ( <CheckmarkIcon /> )} Item </> )} </MenuItem> The states and selectors for each component used in a `Menu` are documented below. ### MenuTrigger# The `MenuTrigger` component does not render any DOM elements (it only passes through its children) so it does not support styling. If you need a wrapper element, add one yourself inside the `<MenuTrigger>`. <MenuTrigger> <div className="my-menu-trigger"> {/* ... */} </div> </MenuTrigger> <MenuTrigger> <div className="my-menu-trigger"> {/* ... */} </div> </MenuTrigger> <MenuTrigger> <div className="my-menu-trigger"> {/* ... */} </div> </MenuTrigger> ### Button# A Button can be targeted with the `.react-aria-Button` CSS selector, or by overriding with a custom `className`. It supports the following states: | Name | CSS Selector | Description | | --- | --- | --- | | `isHovered` | `[data-hovered]` | Whether the button is currently hovered with a mouse. | | `isPressed` | `[data-pressed]` | Whether the button is currently in a pressed state. | | `isFocused` | `[data-focused]` | Whether the button is focused, either via a mouse or keyboard. | | `isFocusVisible` | `[data-focus-visible]` | Whether the button is keyboard focused. | | `isDisabled` | `[data-disabled]` | Whether the button is disabled. | | `isPending` | `[data-pending]` | Whether the button is currently in a pending state. | ### Popover# The Popover component can be targeted with the `.react-aria-Popover` CSS selector, or by overriding with a custom `className`. Note that it renders in a React Portal, so it will not appear as a descendant of the MenuTrigger in the DOM. It supports the following states and render props: | Name | CSS Selector | Description | | --- | --- | --- | | `trigger` | `[data-trigger="..."]` | The name of the component that triggered the popover, e.g. "DialogTrigger" or "ComboBox". | | `placement` | `[data-placement="left | right | top | bottom"]` | The placement of the popover relative to the trigger. | | `isEntering` | `[data-entering]` | Whether the popover is currently entering. Use this to apply animations. | | `isExiting` | `[data-exiting]` | Whether the popover is currently exiting. Use this to apply animations. | Within a MenuTrigger, the popover will have the `data-trigger="MenuTrigger"` attribute, which can be used to define menu-specific styles. In addition, the `--trigger-width` CSS custom property will be set on the popover, which you can use to make the popover match the width of the menu button. .react-aria-Popover[data-trigger=MenuTrigger] { width: var(--trigger-width); } .react-aria-Popover[data-trigger=MenuTrigger] { width: var(--trigger-width); } .react-aria-Popover[data-trigger=MenuTrigger] { width: var(--trigger-width); } Within a SubmenuTrigger, the popover will have the `data-trigger="SubmenuTrigger"` attribute, which can be used to define submenu-specific styles. .react-aria-Popover[data-trigger=SubmenuTrigger][data-placement="right"] { transform: translateX(-5px); } .react-aria-Popover[data-trigger=SubmenuTrigger][data-placement="left"] { transform: translateX(5px); } .react-aria-Popover[data-trigger=SubmenuTrigger][data-placement="right"] { transform: translateX(-5px); } .react-aria-Popover[data-trigger=SubmenuTrigger][data-placement="left"] { transform: translateX(5px); } .react-aria-Popover[data-trigger=SubmenuTrigger][data-placement="right"] { transform: translateX(-5px); } .react-aria-Popover[data-trigger=SubmenuTrigger][data-placement="left"] { transform: translateX(5px); } ### Menu# A `Menu` can be targeted with the `.react-aria-Menu` CSS selector, or by overriding with a custom `className`. ### MenuSection# A `MenuSection` can be targeted with the `.react-aria-MenuSection` CSS selector, or by overriding with a custom `className`. See sections for examples. ### Header# A `Header` within a `MenuSection` can be targeted with the `.react-aria-Header` CSS selector, or by overriding with a custom `className`. See sections for examples. ### MenuItem# A `MenuItem` can be targeted with the `.react-aria-MenuItem` CSS selector, or by overriding with a custom `className`. It supports the following states and render props: | Name | CSS Selector | Description | | --- | --- | --- | | `hasSubmenu` | `[data-has-submenu]` | Whether the item has a submenu. | | `isOpen` | `[data-open]` | Whether the item's submenu is open. | | `isHovered` | `[data-hovered]` | Whether the item is currently hovered with a mouse. | | `isPressed` | `[data-pressed]` | Whether the item is currently in a pressed state. | | `isSelected` | `[data-selected]` | Whether the item is currently selected. | | `isFocused` | `[data-focused]` | Whether the item is currently focused. | | `isFocusVisible` | `[data-focus-visible]` | Whether the item is currently keyboard focused. | | `isDisabled` | `[data-disabled]` | Whether the item is non-interactive, i.e. both selection and actions are disabled and the item may not be focused. Dependent on `disabledKeys` and `disabledBehavior`. | | `selectionMode` | `[data-selection-mode="single | multiple"]` | The type of selection that is allowed in the collection. | | `selectionBehavior` | `—` | The selection behavior for the collection. | MenuItems also support two slots: a label, and a description. When provided using the `<Text>` element, the item will have `aria-labelledby` and `aria-describedby` attributes pointing to these slots, improving screen reader announcement. See text slots for an example. Note that items may not contain interactive children such as buttons, as screen readers will not be able to access them. ### Separator# A `Separator` can be targeted with the `.react-aria-Separator` CSS selector, or by overriding with a custom `className`. ## Advanced customization# * * * ### Composition# If you need to customize one of the components within a `MenuTrigger`, such as `Button` or `Menu`, in many cases you can create a wrapper component. This lets you customize the props passed to the component. function MyMenu(props) { return <Menu {...props} className="my-menu" /> } function MyMenu(props) { return <Menu {...props} className="my-menu" /> } function MyMenu(props) { return ( <Menu {...props} className="my-menu" /> ); } ### Custom children# MenuTrigger passes props to its child components, such as the button and popover, via their associated contexts. These contexts are exported so you can also consume them in your own custom components. This enables you to reuse existing components from your app or component library together with React Aria Components. | Component | Context | Props | Ref | | --- | --- | --- | --- | | `Button` | `ButtonContext` | ` ButtonProps ` | `HTMLButtonElement` | | `Popover` | `PopoverContext` | ` PopoverProps ` | `HTMLElement` | | `Menu` | `MenuContext` | ` MenuProps ` | `HTMLDivElement` | | `Separator` | `SeparatorContext` | ` SeparatorProps ` | `HTMLElement` | | `Text` | `TextContext` | ` TextProps ` | `HTMLElement` | | `Keyboard` | `KeyboardContext` | `HTMLAttributes` | `HTMLElement` | This example consumes from `KeyboardContext` in an existing styled keyboard shortcut component to make it compatible with React Aria Components. The `useContextProps` hook merges the local props and ref with the ones provided via context by Menu. import {KeyboardContext, useContextProps} from 'react-aria-components'; const MyKeyboard = React.forwardRef( ( props: React.HTMLAttributes<HTMLElement>, ref: React.ForwardedRef<HTMLElement> ) => { // Merge the local props and ref with the ones provided via context. [props, ref] = useContextProps(props, ref, KeyboardContext); // ... your existing Keyboard component return <kbd {...props} ref={ref} />; } ); import { KeyboardContext, useContextProps } from 'react-aria-components'; const MyKeyboard = React.forwardRef( ( props: React.HTMLAttributes<HTMLElement>, ref: React.ForwardedRef<HTMLElement> ) => { // Merge the local props and ref with the ones provided via context. [props, ref] = useContextProps( props, ref, KeyboardContext ); // ... your existing Keyboard component return <kbd {...props} ref={ref} />; } ); import { KeyboardContext, useContextProps } from 'react-aria-components'; const MyKeyboard = React .forwardRef( ( props: React.HTMLAttributes< HTMLElement >, ref: React.ForwardedRef< HTMLElement > ) => { // Merge the local props and ref with the ones provided via context. [props, ref] = useContextProps( props, ref, KeyboardContext ); // ... your existing Keyboard component return ( <kbd {...props} ref={ref} /> ); } ); Now you can use `MyKeyboard` within a `Menu`, in place of the builtin React Aria Components `Keyboard`. <Menu> <MenuItem textValue="Paste"> <Text slot="label">Paste</Text> <MyKeyboard>⌘V</MyKeyboard> </MenuItem> {/* ... */} </Menu> <Menu> <MenuItem textValue="Paste"> <Text slot="label">Paste</Text> <MyKeyboard>⌘V</MyKeyboard> </MenuItem> {/* ... */} </Menu> <Menu> <MenuItem textValue="Paste"> <Text slot="label"> Paste </Text> <MyKeyboard> ⌘V </MyKeyboard> </MenuItem> {/* ... */} </Menu> ### Hooks# If you need to customize things further, such as intercepting events or customizing DOM structure, you can drop down to the lower level Hook-based API. React Aria Hooks and Components can be mixed and matched by providing or consuming from the corresponding contexts that are exported for each component. See useMenu for details. This example implements a custom `OptionMenuTrigger` component that intercepts the keyboard and press events returned by `useMenuTrigger` so that the menu only opens if the user holds the Alt key. This allows a button to have a default action, with additional options for power users. import {ButtonContext, MenuContext, OverlayTriggerStateContext, PopoverContext, Provider} from 'react-aria-components'; import {useMenuTriggerState} from 'react-stately'; import {useMenuTrigger} from 'react-aria'; function OptionMenuTrigger(props: MenuTriggerProps) { let state = useMenuTriggerState(props); let ref = React.useRef(null); let { menuTriggerProps, menuProps } = useMenuTrigger(props, state, ref); return ( // Provider is a utility that renders multiple context providers without nesting. <Provider values={[ [ButtonContext, { ...menuTriggerProps, // Intercept events and only forward to useMenuTrigger if alt key is held. onPressStart: (e) => e.altKey && menuTriggerProps.onPressStart(e), onPress: (e) => (e.pointerType !== 'mouse' || e.altKey) && menuTriggerProps.onPress(e), onKeyDown: (e) => e.altKey && menuTriggerProps.onKeyDown(e), ref, isPressed: state.isOpen }], [OverlayTriggerStateContext, state], [PopoverContext, { triggerRef: ref, placement: 'bottom start' }], [MenuContext, menuProps] ]} > {props.children} </Provider> ); } import { ButtonContext, MenuContext, OverlayTriggerStateContext, PopoverContext, Provider } from 'react-aria-components'; import {useMenuTriggerState} from 'react-stately'; import {useMenuTrigger} from 'react-aria'; function OptionMenuTrigger(props: MenuTriggerProps) { let state = useMenuTriggerState(props); let ref = React.useRef(null); let { menuTriggerProps, menuProps } = useMenuTrigger( props, state, ref ); return ( // Provider is a utility that renders multiple context providers without nesting. <Provider values={[ [ButtonContext, { ...menuTriggerProps, // Intercept events and only forward to useMenuTrigger if alt key is held. onPressStart: (e) => e.altKey && menuTriggerProps.onPressStart(e), onPress: (e) => (e.pointerType !== 'mouse' || e.altKey) && menuTriggerProps.onPress(e), onKeyDown: (e) => e.altKey && menuTriggerProps.onKeyDown(e), ref, isPressed: state.isOpen }], [OverlayTriggerStateContext, state], [PopoverContext, { triggerRef: ref, placement: 'bottom start' }], [MenuContext, menuProps] ]} > {props.children} </Provider> ); } import { ButtonContext, MenuContext, OverlayTriggerStateContext, PopoverContext, Provider } from 'react-aria-components'; import {useMenuTriggerState} from 'react-stately'; import {useMenuTrigger} from 'react-aria'; function OptionMenuTrigger( props: MenuTriggerProps ) { let state = useMenuTriggerState( props ); let ref = React.useRef( null ); let { menuTriggerProps, menuProps } = useMenuTrigger( props, state, ref ); return ( // Provider is a utility that renders multiple context providers without nesting. <Provider values={[ [ButtonContext, { ...menuTriggerProps, // Intercept events and only forward to useMenuTrigger if alt key is held. onPressStart: (e) => e.altKey && menuTriggerProps .onPressStart( e ), onPress: (e) => (e.pointerType !== 'mouse' || e.altKey) && menuTriggerProps .onPress( e ), onKeyDown: (e) => e.altKey && menuTriggerProps .onKeyDown( e ), ref, isPressed: state.isOpen }], [ OverlayTriggerStateContext, state ], [ PopoverContext, { triggerRef: ref, placement: 'bottom start' } ], [ MenuContext, menuProps ] ]} > {props.children} </Provider> ); } By providing the above contexts, the existing `Button`, `Popover`, and `Menu` components from React Aria Components can be used with this custom trigger built with the hooks. <OptionMenuTrigger> <Button>Save</Button> <Popover> <Menu> <MenuItem>Save</MenuItem> <MenuItem>Save as…</MenuItem> <MenuItem>Rename…</MenuItem> <MenuItem>Delete…</MenuItem> </Menu> </Popover> </OptionMenuTrigger> <OptionMenuTrigger> <Button>Save</Button> <Popover> <Menu> <MenuItem>Save</MenuItem> <MenuItem>Save as…</MenuItem> <MenuItem>Rename…</MenuItem> <MenuItem>Delete…</MenuItem> </Menu> </Popover> </OptionMenuTrigger> <OptionMenuTrigger> <Button>Save</Button> <Popover> <Menu> <MenuItem> Save </MenuItem> <MenuItem> Save as… </MenuItem> <MenuItem> Rename… </MenuItem> <MenuItem> Delete… </MenuItem> </Menu> </Popover> </OptionMenuTrigger> Save ## Testing# * * * ### Test utils alpha# `@react-aria/test-utils` offers common menu interaction utilities which you may find helpful when writing tests. See here for more information on how to setup these utilities in your tests. Below is the full definition of the menu tester and a sample of how you could use it in your test suite. // Menu.test.ts import {render} from '@testing-library/react'; import {User} from '@react-aria/test-utils'; let testUtilUser = new User({ interactionType: 'mouse' }); // ... it('Menu can open its submenu via keyboard', async function () { // Render your test component/app and initialize the menu tester let { getByTestId } = render( <MenuTrigger> <Button data-testid="test-menutrigger">Menu trigger</Button> ... </MenuTrigger> ); let menuTester = testUtilUser.createTester('Menu', { root: getByTestId('test-menutrigger'), interactionType: 'keyboard' }); await menuTester.open(); expect(menuTester.menu).toBeInTheDocument(); let submenuTriggers = menuTester.submenuTriggers; expect(submenuTriggers).toHaveLength(1); let submenuTester = await menuTester.openSubmenu({ submenuTrigger: 'Share…' }); expect(submenuTester.menu).toBeInTheDocument(); await submenuTester.selectOption({ option: submenuTester.options()[0] }); expect(submenuTester.menu).not.toBeInTheDocument(); expect(menuTester.menu).not.toBeInTheDocument(); }); // Menu.test.ts import {render} from '@testing-library/react'; import {User} from '@react-aria/test-utils'; let testUtilUser = new User({ interactionType: 'mouse' }); // ... it('Menu can open its submenu via keyboard', async function () { // Render your test component/app and initialize the menu tester let { getByTestId } = render( <MenuTrigger> <Button data-testid="test-menutrigger"> Menu trigger </Button> ... </MenuTrigger> ); let menuTester = testUtilUser.createTester('Menu', { root: getByTestId('test-menutrigger'), interactionType: 'keyboard' }); await menuTester.open(); expect(menuTester.menu).toBeInTheDocument(); let submenuTriggers = menuTester.submenuTriggers; expect(submenuTriggers).toHaveLength(1); let submenuTester = await menuTester.openSubmenu({ submenuTrigger: 'Share…' }); expect(submenuTester.menu).toBeInTheDocument(); await submenuTester.selectOption({ option: submenuTester.options()[0] }); expect(submenuTester.menu).not.toBeInTheDocument(); expect(menuTester.menu).not.toBeInTheDocument(); }); // Menu.test.ts import {render} from '@testing-library/react'; import {User} from '@react-aria/test-utils'; let testUtilUser = new User({ interactionType: 'mouse' }); // ... it('Menu can open its submenu via keyboard', async function () { // Render your test component/app and initialize the menu tester let { getByTestId } = render( <MenuTrigger> <Button data-testid="test-menutrigger"> Menu trigger </Button> ... </MenuTrigger> ); let menuTester = testUtilUser .createTester( 'Menu', { root: getByTestId( 'test-menutrigger' ), interactionType: 'keyboard' } ); await menuTester .open(); expect(menuTester.menu) .toBeInTheDocument(); let submenuTriggers = menuTester .submenuTriggers; expect(submenuTriggers) .toHaveLength(1); let submenuTester = await menuTester .openSubmenu({ submenuTrigger: 'Share…' }); expect( submenuTester.menu ).toBeInTheDocument(); await submenuTester .selectOption({ option: submenuTester .options()[0] }); expect( submenuTester.menu ).not .toBeInTheDocument(); expect(menuTester.menu) .not .toBeInTheDocument(); }); ### Properties | Name | Type | Description | | --- | --- | --- | | `trigger` | `HTMLElement` | Returns the menu's trigger. | | `menu` | `HTMLElement | null` | Returns the menu if present. | | `sections` | `HTMLElement[]` | Returns the menu's sections if any. | | `submenuTriggers` | `HTMLElement[]` | Returns the menu's submenu triggers if any. | ### Methods | Method | Description | | --- | --- | | `constructor( (opts: MenuTesterOpts )): void` | | | `setInteractionType( (type: UserOpts ['interactionType'] )): void` | Set the interaction type used by the menu tester. | | `open( (opts: MenuOpenOpts )): Promise<void>` | Opens the menu. Defaults to using the interaction type set on the menu tester. | | `findOption( (opts: { optionIndexOrText: number | | string } )): HTMLElement` | Returns a option matching the specified index or text content. | | `selectOption( (opts: MenuSelectOpts )): Promise<void>` | Selects the desired menu option. Defaults to using the interaction type set on the menu tester. If necessary, will open the menu dropdown beforehand. The desired option can be targeted via the option's node, the option's text, or the option's index. | | `openSubmenu( (opts: MenuOpenSubmenuOpts )): Promise< MenuTester | null>` | Opens the submenu. Defaults to using the interaction type set on the menu tester. The submenu trigger can be targeted via the trigger's node or the trigger's text. | | `close(): Promise<void>` | Closes the menu. | | `options( (opts: { element?: HTMLElement } )): HTMLElement[]` | Returns the menu's options if present. Can be filtered to a subsection of the menu if provided via `element`. | --- ## Page: https://react-spectrum.adobe.com/react-aria/Table.html # Table A table displays data in rows and columns and enables a user to navigate its contents via directional navigation keys, and optionally supports row selection and sorting. <table><tbody><tr><th>install</th><td><code>yarn add react-aria-components</code></td></tr><tr><th>version</th><td>1.10.1</td></tr><tr><th>usage</th><td><code><span>import</span> {Table} <span>from</span> <span>'react-aria-components'</span></code></td></tr></tbody></table> View ARIA pattern W3C View repository GitHub View package NPM ## Example# * * * import {Cell, Column, Row, Table, TableBody, TableHeader} from 'react-aria-components'; import {MyCheckbox} from './Checkbox'; <Table aria-label="Files" selectionMode="multiple"> <TableHeader> <Column> <MyCheckbox slot="selection" /> </Column> <Column isRowHeader>Name</Column> <Column>Type</Column> <Column>Date Modified</Column> </TableHeader> <TableBody> <Row> <Cell> <MyCheckbox slot="selection" /> </Cell> <Cell>Games</Cell> <Cell>File folder</Cell> <Cell>6/7/2020</Cell> </Row> <Row> <Cell> <MyCheckbox slot="selection" /> </Cell> <Cell>Program Files</Cell> <Cell>File folder</Cell> <Cell>4/7/2021</Cell> </Row> <Row> <Cell> <MyCheckbox slot="selection" /> </Cell> <Cell>bootmgr</Cell> <Cell>System file</Cell> <Cell>11/20/2010</Cell> </Row> <Row> <Cell> <MyCheckbox slot="selection" /> </Cell> <Cell>log.txt</Cell> <Cell>Text Document</Cell> <Cell>1/18/2016</Cell> </Row> </TableBody> </Table> import { Cell, Column, Row, Table, TableBody, TableHeader } from 'react-aria-components'; import {MyCheckbox} from './Checkbox'; <Table aria-label="Files" selectionMode="multiple"> <TableHeader> <Column> <MyCheckbox slot="selection" /> </Column> <Column isRowHeader>Name</Column> <Column>Type</Column> <Column>Date Modified</Column> </TableHeader> <TableBody> <Row> <Cell> <MyCheckbox slot="selection" /> </Cell> <Cell>Games</Cell> <Cell>File folder</Cell> <Cell>6/7/2020</Cell> </Row> <Row> <Cell> <MyCheckbox slot="selection" /> </Cell> <Cell>Program Files</Cell> <Cell>File folder</Cell> <Cell>4/7/2021</Cell> </Row> <Row> <Cell> <MyCheckbox slot="selection" /> </Cell> <Cell>bootmgr</Cell> <Cell>System file</Cell> <Cell>11/20/2010</Cell> </Row> <Row> <Cell> <MyCheckbox slot="selection" /> </Cell> <Cell>log.txt</Cell> <Cell>Text Document</Cell> <Cell>1/18/2016</Cell> </Row> </TableBody> </Table> import { Cell, Column, Row, Table, TableBody, TableHeader } from 'react-aria-components'; import {MyCheckbox} from './Checkbox'; <Table aria-label="Files" selectionMode="multiple" > <TableHeader> <Column> <MyCheckbox slot="selection" /> </Column> <Column isRowHeader > Name </Column> <Column> Type </Column> <Column> Date Modified </Column> </TableHeader> <TableBody> <Row> <Cell> <MyCheckbox slot="selection" /> </Cell> <Cell> Games </Cell> <Cell> File folder </Cell> <Cell> 6/7/2020 </Cell> </Row> <Row> <Cell> <MyCheckbox slot="selection" /> </Cell> <Cell> Program Files </Cell> <Cell> File folder </Cell> <Cell> 4/7/2021 </Cell> </Row> <Row> <Cell> <MyCheckbox slot="selection" /> </Cell> <Cell> bootmgr </Cell> <Cell> System file </Cell> <Cell> 11/20/2010 </Cell> </Row> <Row> <Cell> <MyCheckbox slot="selection" /> </Cell> <Cell> log.txt </Cell> <Cell> Text Document </Cell> <Cell> 1/18/2016 </Cell> </Row> </TableBody> </Table> | | Name | Type | Date Modified | | --- | --- | --- | --- | | | Games | File folder | 6/7/2020 | | | Program Files | File folder | 4/7/2021 | | | bootmgr | System file | 11/20/2010 | | | log.txt | Text Document | 1/18/2016 | Show CSS @import "@react-aria/example-theme"; .react-aria-Table { padding: 0.286rem; border: 1px solid var(--border-color); border-radius: 6px; background: var(--overlay-background); outline: none; border-spacing: 0; min-height: 100px; align-self: start; max-width: 100%; word-break: break-word; forced-color-adjust: none; &[data-focus-visible] { outline: 2px solid var(--focus-ring-color); outline-offset: -1px; } .react-aria-TableHeader { color: var(--text-color); &:after { content: ''; display: table-row; height: 2px; } & tr:last-child .react-aria-Column { border-bottom: 1px solid var(--border-color); cursor: default; } } .react-aria-Row { --radius-top: 6px; --radius-bottom: 6px; --radius: var(--radius-top) var(--radius-top) var(--radius-bottom) var(--radius-bottom); border-radius: var(--radius); clip-path: inset(0 round var(--radius)); /* firefox */ outline: none; cursor: default; color: var(--text-color); font-size: 1.072rem; position: relative; transform: scale(1); &[data-focus-visible] { outline: 2px solid var(--focus-ring-color); outline-offset: -2px; } &[data-pressed] { background: var(--gray-100); } &[data-selected] { background: var(--highlight-background); color: var(--highlight-foreground); --focus-ring-color: var(--highlight-foreground); &[data-focus-visible], .react-aria-Cell[data-focus-visible] { outline-offset: -4px; } } &[data-disabled] { color: var(--text-color-disabled); } } .react-aria-Cell, .react-aria-Column { padding: 4px 8px; text-align: left; outline: none; &[data-focus-visible] { outline: 2px solid var(--focus-ring-color); outline-offset: -2px; } } .react-aria-Cell { transform: translateZ(0); &:first-child { border-radius: var(--radius-top) 0 0 var(--radius-bottom); } &:last-child { border-radius: 0 var(--radius-top) var(--radius-bottom) 0; } } /* join selected items if :has selector is supported */ @supports selector(:has(.foo)) { .react-aria-Row[data-selected]:has(+ [data-selected]), .react-aria-Row[data-selected]:has(+ .react-aria-DropIndicator + [data-selected]) { --radius-bottom: 0px; } .react-aria-Row[data-selected] + [data-selected], .react-aria-Row[data-selected] + .react-aria-DropIndicator + [data-selected]{ --radius-top: 0px; } } } :where(.react-aria-Row) .react-aria-Checkbox { --selected-color: var(--highlight-foreground); --selected-color-pressed: var(--highlight-foreground-pressed); --checkmark-color: var(--highlight-background); --background-color: var(--highlight-background); } @import "@react-aria/example-theme"; .react-aria-Table { padding: 0.286rem; border: 1px solid var(--border-color); border-radius: 6px; background: var(--overlay-background); outline: none; border-spacing: 0; min-height: 100px; align-self: start; max-width: 100%; word-break: break-word; forced-color-adjust: none; &[data-focus-visible] { outline: 2px solid var(--focus-ring-color); outline-offset: -1px; } .react-aria-TableHeader { color: var(--text-color); &:after { content: ''; display: table-row; height: 2px; } & tr:last-child .react-aria-Column { border-bottom: 1px solid var(--border-color); cursor: default; } } .react-aria-Row { --radius-top: 6px; --radius-bottom: 6px; --radius: var(--radius-top) var(--radius-top) var(--radius-bottom) var(--radius-bottom); border-radius: var(--radius); clip-path: inset(0 round var(--radius)); /* firefox */ outline: none; cursor: default; color: var(--text-color); font-size: 1.072rem; position: relative; transform: scale(1); &[data-focus-visible] { outline: 2px solid var(--focus-ring-color); outline-offset: -2px; } &[data-pressed] { background: var(--gray-100); } &[data-selected] { background: var(--highlight-background); color: var(--highlight-foreground); --focus-ring-color: var(--highlight-foreground); &[data-focus-visible], .react-aria-Cell[data-focus-visible] { outline-offset: -4px; } } &[data-disabled] { color: var(--text-color-disabled); } } .react-aria-Cell, .react-aria-Column { padding: 4px 8px; text-align: left; outline: none; &[data-focus-visible] { outline: 2px solid var(--focus-ring-color); outline-offset: -2px; } } .react-aria-Cell { transform: translateZ(0); &:first-child { border-radius: var(--radius-top) 0 0 var(--radius-bottom); } &:last-child { border-radius: 0 var(--radius-top) var(--radius-bottom) 0; } } /* join selected items if :has selector is supported */ @supports selector(:has(.foo)) { .react-aria-Row[data-selected]:has(+ [data-selected]), .react-aria-Row[data-selected]:has(+ .react-aria-DropIndicator + [data-selected]) { --radius-bottom: 0px; } .react-aria-Row[data-selected] + [data-selected], .react-aria-Row[data-selected] + .react-aria-DropIndicator + [data-selected]{ --radius-top: 0px; } } } :where(.react-aria-Row) .react-aria-Checkbox { --selected-color: var(--highlight-foreground); --selected-color-pressed: var(--highlight-foreground-pressed); --checkmark-color: var(--highlight-background); --background-color: var(--highlight-background); } @import "@react-aria/example-theme"; .react-aria-Table { padding: 0.286rem; border: 1px solid var(--border-color); border-radius: 6px; background: var(--overlay-background); outline: none; border-spacing: 0; min-height: 100px; align-self: start; max-width: 100%; word-break: break-word; forced-color-adjust: none; &[data-focus-visible] { outline: 2px solid var(--focus-ring-color); outline-offset: -1px; } .react-aria-TableHeader { color: var(--text-color); &:after { content: ''; display: table-row; height: 2px; } & tr:last-child .react-aria-Column { border-bottom: 1px solid var(--border-color); cursor: default; } } .react-aria-Row { --radius-top: 6px; --radius-bottom: 6px; --radius: var(--radius-top) var(--radius-top) var(--radius-bottom) var(--radius-bottom); border-radius: var(--radius); clip-path: inset(0 round var(--radius)); /* firefox */ outline: none; cursor: default; color: var(--text-color); font-size: 1.072rem; position: relative; transform: scale(1); &[data-focus-visible] { outline: 2px solid var(--focus-ring-color); outline-offset: -2px; } &[data-pressed] { background: var(--gray-100); } &[data-selected] { background: var(--highlight-background); color: var(--highlight-foreground); --focus-ring-color: var(--highlight-foreground); &[data-focus-visible], .react-aria-Cell[data-focus-visible] { outline-offset: -4px; } } &[data-disabled] { color: var(--text-color-disabled); } } .react-aria-Cell, .react-aria-Column { padding: 4px 8px; text-align: left; outline: none; &[data-focus-visible] { outline: 2px solid var(--focus-ring-color); outline-offset: -2px; } } .react-aria-Cell { transform: translateZ(0); &:first-child { border-radius: var(--radius-top) 0 0 var(--radius-bottom); } &:last-child { border-radius: 0 var(--radius-top) var(--radius-bottom) 0; } } /* join selected items if :has selector is supported */ @supports selector(:has(.foo)) { .react-aria-Row[data-selected]:has(+ [data-selected]), .react-aria-Row[data-selected]:has(+ .react-aria-DropIndicator + [data-selected]) { --radius-bottom: 0px; } .react-aria-Row[data-selected] + [data-selected], .react-aria-Row[data-selected] + .react-aria-DropIndicator + [data-selected]{ --radius-top: 0px; } } } :where(.react-aria-Row) .react-aria-Checkbox { --selected-color: var(--highlight-foreground); --selected-color-pressed: var(--highlight-foreground-pressed); --checkmark-color: var(--highlight-background); --background-color: var(--highlight-background); } ## Features# * * * A table can be built using the <table>, <tr>, <td>, and other table specific HTML elements, but is very limited in functionality especially when it comes to user interactions. HTML tables are meant for static content, rather than tables with rich interactions like focusable elements within cells, keyboard navigation, row selection, sorting, etc. `Table` helps achieve accessible and interactive table components that can be styled as needed. * **Row selection** – Single or multiple selection, with optional checkboxes, disabled rows, and both `toggle` and `replace` selection behaviors. * **Columns** – Support for column sorting and row header columns. Columns may optionally allow user resizing via mouse, touch, and keyboard interactions. * **Interactive children** – Table cells may include interactive elements such as buttons, menus, etc. * **Actions** – Rows and cells support optional actions such as navigation via click, tap, double click, or Enter key. * **Async loading** – Support for loading and sorting items asynchronously. * **Keyboard navigation** – Table rows, cells, and focusable children can be navigated using the arrow keys, along with page up/down, home/end, etc. Typeahead, auto scrolling, and selection modifier keys are supported as well. * **Drag and drop** – Tables support drag and drop to reorder, insert, or update rows via mouse, touch, keyboard, and screen reader interactions. * **Virtualized scrolling** – Use Virtualizer to improve performance of large tables by rendering only the visible rows. * **Touch friendly** – Selection and actions adapt their behavior depending on the device. For example, selection is activated via long press on touch when row actions are present. * **Accessible** – Follows the ARIA grid pattern, with additional selection announcements via an ARIA live region. Extensively tested across many devices and assistive technologies to ensure announcements and behaviors are consistent. ## Anatomy# * * * A table consists of a container element, with columns and rows of cells containing data inside. The cells within a table may contain focusable elements or plain text content. If the table supports row selection, each row can optionally include a selection checkbox. Additionally, a "select all" checkbox may be displayed in a column header if the table supports multiple row selection. A drag button may also be included within a cell if the row is draggable. If a table supports column resizing, then it should also be wrapped in a `<ResizableTableContainer>`, and a `<ColumnResizer>` should be included in each resizable column. import {Button, Cell, Checkbox, Column, ColumnResizer, ResizableTableContainer, Row, Table, TableBody, TableHeader} from 'react-aria-components'; <ResizableTableContainer> <Table> <TableHeader> <Column /> <Column> <Checkbox slot="selection" /> </Column> <Column> <ColumnResizer /> </Column> <Column> <Column /> <Column /> </Column> </TableHeader> <TableBody> <Row> <Cell> <Button slot="drag" /> </Cell> <Cell> <Checkbox slot="selection" /> </Cell> <Cell /> <Cell /> <Cell /> </Row> </TableBody> </Table> </ResizableTableContainer> import { Button, Cell, Checkbox, Column, ColumnResizer, ResizableTableContainer, Row, Table, TableBody, TableHeader } from 'react-aria-components'; <ResizableTableContainer> <Table> <TableHeader> <Column /> <Column> <Checkbox slot="selection" /> </Column> <Column> <ColumnResizer /> </Column> <Column> <Column /> <Column /> </Column> </TableHeader> <TableBody> <Row> <Cell> <Button slot="drag" /> </Cell> <Cell> <Checkbox slot="selection" /> </Cell> <Cell /> <Cell /> <Cell /> </Row> </TableBody> </Table> </ResizableTableContainer> import { Button, Cell, Checkbox, Column, ColumnResizer, ResizableTableContainer, Row, Table, TableBody, TableHeader } from 'react-aria-components'; <ResizableTableContainer> <Table> <TableHeader> <Column /> <Column> <Checkbox slot="selection" /> </Column> <Column> <ColumnResizer /> </Column> <Column> <Column /> <Column /> </Column> </TableHeader> <TableBody> <Row> <Cell> <Button slot="drag" /> </Cell> <Cell> <Checkbox slot="selection" /> </Cell> <Cell /> <Cell /> <Cell /> </Row> </TableBody> </Table> </ResizableTableContainer> ### Concepts# `Table` makes use of the following concepts: Collections Defining collections of items, async loading, and updating items over time. Selection Interactions and data structures to represent selection. Drag and drop Concepts and interactions for an accessible drag and drop experience. ### Composed components# A `Table` uses the following components, which may also be used standalone or reused in other components. Checkbox A checkbox allows a user to select an individual option. Button A button allows a user to perform an action. ## Examples# * * * Stock Table A table with sticky headers, sorting, multiple selection, and column resizing. ## Starter kits# * * * To help kick-start your project, we offer starter kits that include example implementations of all React Aria components with various styling solutions. All components are fully styled, including support for dark mode, high contrast mode, and all UI states. Each starter comes with a pre-configured Storybook that you can experiment with, or use as a starting point for your own component library. Vanilla CSS Download ZIP Preview Tailwind CSS Download ZIP Preview ## Reusable wrappers# * * * If you will use a Table in multiple places in your app, you can wrap all of the pieces into a reusable component. This way, the DOM structure, styling code, and other logic are defined in a single place and reused everywhere to ensure consistency. The following example includes a custom Column component with a sort indicator. It displays an upwards facing arrow when the column is sorted in the ascending direction, and a downward facing arrow otherwise. import type {ColumnProps} from 'react-aria-components'; export function MyColumn( props: Omit<ColumnProps, 'children'> & { children?: React.ReactNode } ) { return ( <Column {...props}> {({ allowsSorting, sortDirection }) => ( <> {props.children} {allowsSorting && ( <span aria-hidden="true" className="sort-indicator"> {sortDirection === 'ascending' ? '▲' : '▼'} </span> )} </> )} </Column> ); } import type {ColumnProps} from 'react-aria-components'; export function MyColumn( props: Omit<ColumnProps, 'children'> & { children?: React.ReactNode; } ) { return ( <Column {...props}> {({ allowsSorting, sortDirection }) => ( <> {props.children} {allowsSorting && ( <span aria-hidden="true" className="sort-indicator" > {sortDirection === 'ascending' ? '▲' : '▼'} </span> )} </> )} </Column> ); } import type {ColumnProps} from 'react-aria-components'; export function MyColumn( props: & Omit< ColumnProps, 'children' > & { children?: React.ReactNode; } ) { return ( <Column {...props}> {( { allowsSorting, sortDirection } ) => ( <> {props .children} {allowsSorting && ( <span aria-hidden="true" className="sort-indicator" > {sortDirection === 'ascending' ? '▲' : '▼'} </span> )} </> )} </Column> ); } The TableHeader and Row components can also be wrapped to automatically include checkboxes for selection, and a drag handle when drag and drop is enabled, allowing consumers to avoid repeating them in each row. In this example, the select all checkbox is displayed when multiple selection is enabled and the selection behavior is "toggle". These options can be retrieved from the table using the `useTableOptions` hook. We also use the` Collection `component to generate children from either static or dynamic collections the same way as the default TableHeader and Row components. import type {RowProps, TableHeaderProps} from 'react-aria-components'; import {Collection, useTableOptions} from 'react-aria-components'; export function MyTableHeader<T extends object>( { columns, children }: TableHeaderProps<T> ) { let { selectionBehavior, selectionMode, allowsDragging } = useTableOptions(); return ( <TableHeader> {/* Add extra columns for drag and drop and selection. */} {allowsDragging && <Column />} {selectionBehavior === 'toggle' && ( <Column> {selectionMode === 'multiple' && <MyCheckbox slot="selection" />} </Column> )} <Collection items={columns}> {children} </Collection> </TableHeader> ); } export function MyRow<T extends object>( { id, columns, children, ...otherProps }: RowProps<T> ) { let { selectionBehavior, allowsDragging } = useTableOptions(); return ( <Row id={id} {...otherProps}> {allowsDragging && ( <Cell> <Button slot="drag">≡</Button> </Cell> )} {selectionBehavior === 'toggle' && ( <Cell> <MyCheckbox slot="selection" /> </Cell> )} <Collection items={columns}> {children} </Collection> </Row> ); } import type { RowProps, TableHeaderProps } from 'react-aria-components'; import { Collection, useTableOptions } from 'react-aria-components'; export function MyTableHeader<T extends object>( { columns, children }: TableHeaderProps<T> ) { let { selectionBehavior, selectionMode, allowsDragging } = useTableOptions(); return ( <TableHeader> {/* Add extra columns for drag and drop and selection. */} {allowsDragging && <Column />} {selectionBehavior === 'toggle' && ( <Column> {selectionMode === 'multiple' && ( <MyCheckbox slot="selection" /> )} </Column> )} <Collection items={columns}> {children} </Collection> </TableHeader> ); } export function MyRow<T extends object>( { id, columns, children, ...otherProps }: RowProps<T> ) { let { selectionBehavior, allowsDragging } = useTableOptions(); return ( <Row id={id} {...otherProps}> {allowsDragging && ( <Cell> <Button slot="drag">≡</Button> </Cell> )} {selectionBehavior === 'toggle' && ( <Cell> <MyCheckbox slot="selection" /> </Cell> )} <Collection items={columns}> {children} </Collection> </Row> ); } import type { RowProps, TableHeaderProps } from 'react-aria-components'; import { Collection, useTableOptions } from 'react-aria-components'; export function MyTableHeader< T extends object >( { columns, children }: TableHeaderProps<T> ) { let { selectionBehavior, selectionMode, allowsDragging } = useTableOptions(); return ( <TableHeader> {/* Add extra columns for drag and drop and selection. */} {allowsDragging && <Column />} {selectionBehavior === 'toggle' && ( <Column> {selectionMode === 'multiple' && ( <MyCheckbox slot="selection" /> )} </Column> )} <Collection items={columns} > {children} </Collection> </TableHeader> ); } export function MyRow< T extends object >( { id, columns, children, ...otherProps }: RowProps<T> ) { let { selectionBehavior, allowsDragging } = useTableOptions(); return ( <Row id={id} {...otherProps} > {allowsDragging && ( <Cell> <Button slot="drag"> ≡ </Button> </Cell> )} {selectionBehavior === 'toggle' && ( <Cell> <MyCheckbox slot="selection" /> </Cell> )} <Collection items={columns} > {children} </Collection> </Row> ); } Now we can render a table with a default selection column built in. <Table aria-label="Files" selectionMode="multiple"> <MyTableHeader> <MyColumn isRowHeader>Name</MyColumn> <MyColumn>Type</MyColumn> <MyColumn>Date Modified</MyColumn> </MyTableHeader> <TableBody> <MyRow> <Cell>Games</Cell> <Cell>File folder</Cell> <Cell>6/7/2020</Cell> </MyRow> <MyRow> <Cell>Program Files</Cell> <Cell>File folder</Cell> <Cell>4/7/2021</Cell> </MyRow> <MyRow> <Cell>bootmgr</Cell> <Cell>System file</Cell> <Cell>11/20/2010</Cell> </MyRow> </TableBody> </Table> <Table aria-label="Files" selectionMode="multiple"> <MyTableHeader> <MyColumn isRowHeader>Name</MyColumn> <MyColumn>Type</MyColumn> <MyColumn>Date Modified</MyColumn> </MyTableHeader> <TableBody> <MyRow> <Cell>Games</Cell> <Cell>File folder</Cell> <Cell>6/7/2020</Cell> </MyRow> <MyRow> <Cell>Program Files</Cell> <Cell>File folder</Cell> <Cell>4/7/2021</Cell> </MyRow> <MyRow> <Cell>bootmgr</Cell> <Cell>System file</Cell> <Cell>11/20/2010</Cell> </MyRow> </TableBody> </Table> <Table aria-label="Files" selectionMode="multiple" > <MyTableHeader> <MyColumn isRowHeader > Name </MyColumn> <MyColumn> Type </MyColumn> <MyColumn> Date Modified </MyColumn> </MyTableHeader> <TableBody> <MyRow> <Cell> Games </Cell> <Cell> File folder </Cell> <Cell> 6/7/2020 </Cell> </MyRow> <MyRow> <Cell> Program Files </Cell> <Cell> File folder </Cell> <Cell> 4/7/2021 </Cell> </MyRow> <MyRow> <Cell> bootmgr </Cell> <Cell> System file </Cell> <Cell> 11/20/2010 </Cell> </MyRow> </TableBody> </Table> | | Name | Type | Date Modified | | --- | --- | --- | --- | | | Games | File folder | 6/7/2020 | | | Program Files | File folder | 4/7/2021 | | | bootmgr | System file | 11/20/2010 | ## Content# * * * So far, our examples have shown static collections, where the data is hard coded. Dynamic collections, as shown below, can be used when the table data comes from an external data source such as an API, or updates over time. In the example below, both the columns and the rows are provided to the table via a render function, enabling the user to hide and show columns and add additional rows. You can also make the columns static and only the rows dynamic. **Note**: Dynamic collections are automatically memoized to improve performance. Use the `dependencies` prop to invalidate cached elements that depend on external state (e.g. `columns` in this example). See the collections guide for more details. import type {TableProps} from 'react-aria-components'; import {MyCheckboxGroup} from './CheckboxGroup'; import {MyCheckbox} from './Checkbox'; function FileTable(props: TableProps) { let [showColumns, setShowColumns] = React.useState(['name', 'type', 'date']); let columns = [ { name: 'Name', id: 'name', isRowHeader: true }, { name: 'Type', id: 'type' }, { name: 'Date Modified', id: 'date' } ].filter((column) => showColumns.includes(column.id)); let [rows, setRows] = React.useState([ { id: 1, name: 'Games', date: '6/7/2020', type: 'File folder' }, { id: 2, name: 'Program Files', date: '4/7/2021', type: 'File folder' }, { id: 3, name: 'bootmgr', date: '11/20/2010', type: 'System file' }, { id: 4, name: 'log.txt', date: '1/18/2016', type: 'Text Document' } ]); let addRow = () => { let date = new Date().toLocaleDateString(); setRows((rows) => [ ...rows, { id: rows.length + 1, name: 'file.txt', date, type: 'Text Document' } ]); }; return ( <div className="flex-col"> <MyCheckboxGroup aria-label="Show columns" value={showColumns} onChange={setShowColumns} style={{ flexDirection: 'row' }} > <MyCheckbox value="type">Type</MyCheckbox> <MyCheckbox value="date">Date Modified</MyCheckbox> </MyCheckboxGroup> <Table aria-label="Files" {...props}> <MyTableHeader columns={columns}> {(column) => ( <Column isRowHeader={column.isRowHeader}> {column.name} </Column> )} </MyTableHeader> <TableBody items={rows} dependencies={[columns]}> {(item) => ( <MyRow columns={columns}> {(column) => <Cell>{item[column.id]}</Cell>} </MyRow> )} </TableBody> </Table> <Button onPress={addRow}>Add row</Button> </div> ); } import type {TableProps} from 'react-aria-components'; import {MyCheckboxGroup} from './CheckboxGroup'; import {MyCheckbox} from './Checkbox'; function FileTable(props: TableProps) { let [showColumns, setShowColumns] = React.useState([ 'name', 'type', 'date' ]); let columns = [ { name: 'Name', id: 'name', isRowHeader: true }, { name: 'Type', id: 'type' }, { name: 'Date Modified', id: 'date' } ].filter((column) => showColumns.includes(column.id)); let [rows, setRows] = React.useState([ { id: 1, name: 'Games', date: '6/7/2020', type: 'File folder' }, { id: 2, name: 'Program Files', date: '4/7/2021', type: 'File folder' }, { id: 3, name: 'bootmgr', date: '11/20/2010', type: 'System file' }, { id: 4, name: 'log.txt', date: '1/18/2016', type: 'Text Document' } ]); let addRow = () => { let date = new Date().toLocaleDateString(); setRows((rows) => [ ...rows, { id: rows.length + 1, name: 'file.txt', date, type: 'Text Document' } ]); }; return ( <div className="flex-col"> <MyCheckboxGroup aria-label="Show columns" value={showColumns} onChange={setShowColumns} style={{ flexDirection: 'row' }} > <MyCheckbox value="type">Type</MyCheckbox> <MyCheckbox value="date">Date Modified</MyCheckbox> </MyCheckboxGroup> <Table aria-label="Files" {...props}> <MyTableHeader columns={columns}> {(column) => ( <Column isRowHeader={column.isRowHeader}> {column.name} </Column> )} </MyTableHeader> <TableBody items={rows} dependencies={[columns]}> {(item) => ( <MyRow columns={columns}> {(column) => <Cell>{item[column.id]}</Cell>} </MyRow> )} </TableBody> </Table> <Button onPress={addRow}>Add row</Button> </div> ); } import type {TableProps} from 'react-aria-components'; import {MyCheckboxGroup} from './CheckboxGroup'; import {MyCheckbox} from './Checkbox'; function FileTable( props: TableProps ) { let [ showColumns, setShowColumns ] = React.useState([ 'name', 'type', 'date' ]); let columns = [ { name: 'Name', id: 'name', isRowHeader: true }, { name: 'Type', id: 'type' }, { name: 'Date Modified', id: 'date' } ].filter((column) => showColumns.includes( column.id ) ); let [rows, setRows] = React.useState([ { id: 1, name: 'Games', date: '6/7/2020', type: 'File folder' }, { id: 2, name: 'Program Files', date: '4/7/2021', type: 'File folder' }, { id: 3, name: 'bootmgr', date: '11/20/2010', type: 'System file' }, { id: 4, name: 'log.txt', date: '1/18/2016', type: 'Text Document' } ]); let addRow = () => { let date = new Date() .toLocaleDateString(); setRows((rows) => [ ...rows, { id: rows.length + 1, name: 'file.txt', date, type: 'Text Document' } ]); }; return ( <div className="flex-col"> <MyCheckboxGroup aria-label="Show columns" value={showColumns} onChange={setShowColumns} style={{ flexDirection: 'row' }} > <MyCheckbox value="type"> Type </MyCheckbox> <MyCheckbox value="date"> Date Modified </MyCheckbox> </MyCheckboxGroup> <Table aria-label="Files" {...props} > <MyTableHeader columns={columns} > {(column) => ( <Column isRowHeader={column .isRowHeader} > {column .name} </Column> )} </MyTableHeader> <TableBody items={rows} dependencies={[ columns ]} > {(item) => ( <MyRow columns={columns} > {(column) => ( <Cell> {item[ column .id ]} </Cell> )} </MyRow> )} </TableBody> </Table> <Button onPress={addRow} > Add row </Button> </div> ); } Type Date Modified | Name | Type | Date Modified | | --- | --- | --- | | Games | File folder | 6/7/2020 | | Program Files | File folder | 4/7/2021 | | bootmgr | System file | 11/20/2010 | | log.txt | Text Document | 1/18/2016 | Add row ## Selection# * * * ### Single selection# By default, `Table` doesn't allow row selection but this can be enabled using the `selectionMode` prop. Use `defaultSelectedKeys` to provide a default set of selected rows. Note that the value of the selected keys must match the `id` prop of the row. The example below enables single selection mode, and uses `defaultSelectedKeys` to select the row with id equal to `2`. A user can click on a different row to change the selection, or click on the same row again to deselect it entirely. // Using the example above <FileTable selectionMode="single" defaultSelectedKeys={[2]} /> // Using the example above <FileTable selectionMode="single" defaultSelectedKeys={[2]} /> // Using the example above <FileTable selectionMode="single" defaultSelectedKeys={[ 2 ]} /> Type Date Modified | | Name | Type | Date Modified | | --- | --- | --- | --- | | | Games | File folder | 6/7/2020 | | | Program Files | File folder | 4/7/2021 | | | bootmgr | System file | 11/20/2010 | | | log.txt | Text Document | 1/18/2016 | Add row ### Multiple selection# Multiple selection can be enabled by setting `selectionMode` to `multiple`. // Using the example above <FileTable selectionMode="multiple" defaultSelectedKeys={[2, 4]} /> // Using the example above <FileTable selectionMode="multiple" defaultSelectedKeys={[2, 4]} /> // Using the example above <FileTable selectionMode="multiple" defaultSelectedKeys={[ 2, 4 ]} /> Type Date Modified | | Name | Type | Date Modified | | --- | --- | --- | --- | | | Games | File folder | 6/7/2020 | | | Program Files | File folder | 4/7/2021 | | | bootmgr | System file | 11/20/2010 | | | log.txt | Text Document | 1/18/2016 | Add row ### Disallow empty selection# Table also supports a `disallowEmptySelection` prop which forces the user to have at least one row in the Table selected at all times. In this mode, if a single row is selected and the user presses it, it will not be deselected. // Using the example above <FileTable selectionMode="single" defaultSelectedKeys={[2]} disallowEmptySelection /> // Using the example above <FileTable selectionMode="single" defaultSelectedKeys={[2]} disallowEmptySelection /> // Using the example above <FileTable selectionMode="single" defaultSelectedKeys={[ 2 ]} disallowEmptySelection /> Type Date Modified | | Name | Type | Date Modified | | --- | --- | --- | --- | | | Games | File folder | 6/7/2020 | | | Program Files | File folder | 4/7/2021 | | | bootmgr | System file | 11/20/2010 | | | log.txt | Text Document | 1/18/2016 | Add row ### Controlled selection# To programmatically control row selection, use the `selectedKeys` prop paired with the `onSelectionChange` callback. The `id` prop from the selected rows will be passed into the callback when the row is pressed, allowing you to update state accordingly. import type {Selection} from 'react-aria-components'; interface Pokemon { id: number, name: string, type: string, level: string } interface PokemonTableProps extends TableProps { items?: Pokemon[], renderEmptyState?: () => string } function PokemonTable(props: PokemonTableProps) { let items = props.items || [ {id: 1, name: 'Charizard', type: 'Fire, Flying', level: '67'}, {id: 2, name: 'Blastoise', type: 'Water', level: '56'}, {id: 3, name: 'Venusaur', type: 'Grass, Poison', level: '83'}, {id: 4, name: 'Pikachu', type: 'Electric', level: '100'} ]; let [selectedKeys, setSelectedKeys] = React.useState<Selection>(new Set()); return ( <Table aria-label="Pokemon table" {...props} selectedKeys={selectedKeys} onSelectionChange={setSelectedKeys} > <MyTableHeader> <Column isRowHeader>Name</Column> <Column>Type</Column> <Column>Level</Column> </MyTableHeader> <TableBody items={items} renderEmptyState={props.renderEmptyState}> {item => ( <MyRow> <Cell>{item.name}</Cell> <Cell>{item.type}</Cell> <Cell>{item.level}</Cell> </MyRow> )} </TableBody> </Table> ); } <PokemonTable selectionMode="multiple" /> import type {Selection} from 'react-aria-components'; interface Pokemon { id: number; name: string; type: string; level: string; } interface PokemonTableProps extends TableProps { items?: Pokemon[]; renderEmptyState?: () => string; } function PokemonTable(props: PokemonTableProps) { let items = props.items || [ { id: 1, name: 'Charizard', type: 'Fire, Flying', level: '67' }, { id: 2, name: 'Blastoise', type: 'Water', level: '56' }, { id: 3, name: 'Venusaur', type: 'Grass, Poison', level: '83' }, { id: 4, name: 'Pikachu', type: 'Electric', level: '100' } ]; let [selectedKeys, setSelectedKeys] = React.useState< Selection >(new Set()); return ( <Table aria-label="Pokemon table" {...props} selectedKeys={selectedKeys} onSelectionChange={setSelectedKeys} > <MyTableHeader> <Column isRowHeader>Name</Column> <Column>Type</Column> <Column>Level</Column> </MyTableHeader> <TableBody items={items} renderEmptyState={props.renderEmptyState} > {(item) => ( <MyRow> <Cell>{item.name}</Cell> <Cell>{item.type}</Cell> <Cell>{item.level}</Cell> </MyRow> )} </TableBody> </Table> ); } <PokemonTable selectionMode="multiple" /> import type {Selection} from 'react-aria-components'; interface Pokemon { id: number; name: string; type: string; level: string; } interface PokemonTableProps extends TableProps { items?: Pokemon[]; renderEmptyState?: () => string; } function PokemonTable( props: PokemonTableProps ) { let items = props.items || [ { id: 1, name: 'Charizard', type: 'Fire, Flying', level: '67' }, { id: 2, name: 'Blastoise', type: 'Water', level: '56' }, { id: 3, name: 'Venusaur', type: 'Grass, Poison', level: '83' }, { id: 4, name: 'Pikachu', type: 'Electric', level: '100' } ]; let [ selectedKeys, setSelectedKeys ] = React.useState< Selection >(new Set()); return ( <Table aria-label="Pokemon table" {...props} selectedKeys={selectedKeys} onSelectionChange={setSelectedKeys} > <MyTableHeader> <Column isRowHeader > Name </Column> <Column> Type </Column> <Column> Level </Column> </MyTableHeader> <TableBody items={items} renderEmptyState={props .renderEmptyState} > {(item) => ( <MyRow> <Cell> {item.name} </Cell> <Cell> {item.type} </Cell> <Cell> {item .level} </Cell> </MyRow> )} </TableBody> </Table> ); } <PokemonTable selectionMode="multiple" /> | | Name | Type | Level | | --- | --- | --- | --- | | | Charizard | Fire, Flying | 67 | | | Blastoise | Water | 56 | | | Venusaur | Grass, Poison | 83 | | | Pikachu | Electric | 100 | ### Selection behavior# By default, `Table` uses the `"toggle"` selection behavior, which behaves like a checkbox group: clicking, tapping, or pressing the Space or Enter keys toggles selection for the focused row. Using the arrow keys moves focus but does not change selection. The `"toggle"` selection mode is often paired with a column of checkboxes in each row as an explicit affordance for selection. When the `selectionBehavior` prop is set to `"replace"`, clicking a row with the mouse _replaces_ the selection with only that row. Using the arrow keys moves both focus and selection. To select multiple rows, modifier keys such as Ctrl, Cmd, and Shift can be used. To move focus without moving selection, the Ctrl key on Windows or the Option key on macOS can be held while pressing the arrow keys. Holding this modifier while pressing the Space key toggles selection for the focused row, which allows multiple selection of non-contiguous items. On touch screen devices, selection always behaves as toggle since modifier keys may not be available. This behavior emulates native platforms such as macOS and Windows, and is often used when checkboxes in each row are not desired. <PokemonTable selectionMode="multiple" selectionBehavior="replace" /> <PokemonTable selectionMode="multiple" selectionBehavior="replace" /> <PokemonTable selectionMode="multiple" selectionBehavior="replace" /> | Name | Type | Level | | --- | --- | --- | | Charizard | Fire, Flying | 67 | | Blastoise | Water | 56 | | Venusaur | Grass, Poison | 83 | | Pikachu | Electric | 100 | ## Row actions# * * * `Table` supports row actions via the `onRowAction` prop, which is useful for functionality such as navigation. In the default `"toggle"` selection behavior, when nothing is selected, clicking or tapping the row triggers the row action. When at least one item is selected, the table is in selection mode, and clicking or tapping a row toggles the selection. Actions may also be triggered via the Enter key, and selection using the Space key. This behavior is slightly different in the `"replace"` selection behavior, where single clicking selects the row and actions are performed via double click. On touch devices, the action becomes the primary tap interaction, and a long press enters into selection mode, which temporarily swaps the selection behavior to `"toggle"` to perform selection (you may wish to display checkboxes when this happens). Deselecting all items exits selection mode and reverts the selection behavior back to `"replace"`. Keyboard behaviors are unaffected. <div style={{display: 'flex', flexWrap: 'wrap', gap: '24px'}}> <PokemonTable aria-label="Pokemon table with row actions and toggle selection behavior" onRowAction={key => alert(`Opening item ${key}...`)} selectionMode="multiple" /> <PokemonTable aria-label="Pokemon table with row actions and replace selection behavior" onRowAction={key => alert(`Opening item ${key}...`)} selectionBehavior="replace" selectionMode="multiple" /> </div> <div style={{ display: 'flex', flexWrap: 'wrap', gap: '24px' }} > <PokemonTable aria-label="Pokemon table with row actions and toggle selection behavior" onRowAction={(key) => alert(`Opening item ${key}...`)} selectionMode="multiple" /> <PokemonTable aria-label="Pokemon table with row actions and replace selection behavior" onRowAction={(key) => alert(`Opening item ${key}...`)} selectionBehavior="replace" selectionMode="multiple" /> </div> <div style={{ display: 'flex', flexWrap: 'wrap', gap: '24px' }} > <PokemonTable aria-label="Pokemon table with row actions and toggle selection behavior" onRowAction={(key) => alert( `Opening item ${key}...` )} selectionMode="multiple" /> <PokemonTable aria-label="Pokemon table with row actions and replace selection behavior" onRowAction={(key) => alert( `Opening item ${key}...` )} selectionBehavior="replace" selectionMode="multiple" /> </div> | | Name | Type | Level | | --- | --- | --- | --- | | | Charizard | Fire, Flying | 67 | | | Blastoise | Water | 56 | | | Venusaur | Grass, Poison | 83 | | | Pikachu | Electric | 100 | | Name | Type | Level | | --- | --- | --- | | Charizard | Fire, Flying | 67 | | Blastoise | Water | 56 | | Venusaur | Grass, Poison | 83 | | Pikachu | Electric | 100 | Rows may also have a row action specified by directly applying `onAction` on the `Row` itself. This may be especially convenient in static collections. If `onAction` is also provided to the `Table`, both the table's and the row's `onAction` are called. <Table aria-label="Table with onAction applied on the rows directly" selectionMode="multiple" > <MyTableHeader> <Column isRowHeader>Name</Column> <Column>Type</Column> <Column>Level</Column> </MyTableHeader> <TableBody> <MyRow onAction={() => alert(`Opening Charizard`)}> <Cell>Charizard</Cell> <Cell>Fire, Flying</Cell> <Cell>67</Cell> </MyRow> <MyRow onAction={() => alert(`Opening Blastoise`)}> <Cell>Blastoise</Cell> <Cell>Water</Cell> <Cell>56</Cell> </MyRow> <MyRow onAction={() => alert(`Opening Venusaur`)}> <Cell>Venusaur</Cell> <Cell>Grass, Poison</Cell> <Cell>83</Cell> </MyRow> <MyRow onAction={() => alert(`Opening Pikachu`)}> <Cell>Pikachu</Cell> <Cell>Electric</Cell> <Cell>100</Cell> </MyRow> </TableBody> </Table> <Table aria-label="Table with onAction applied on the rows directly" selectionMode="multiple" > <MyTableHeader> <Column isRowHeader>Name</Column> <Column>Type</Column> <Column>Level</Column> </MyTableHeader> <TableBody> <MyRow onAction={() => alert(`Opening Charizard`)}> <Cell>Charizard</Cell> <Cell>Fire, Flying</Cell> <Cell>67</Cell> </MyRow> <MyRow onAction={() => alert(`Opening Blastoise`)}> <Cell>Blastoise</Cell> <Cell>Water</Cell> <Cell>56</Cell> </MyRow> <MyRow onAction={() => alert(`Opening Venusaur`)}> <Cell>Venusaur</Cell> <Cell>Grass, Poison</Cell> <Cell>83</Cell> </MyRow> <MyRow onAction={() => alert(`Opening Pikachu`)}> <Cell>Pikachu</Cell> <Cell>Electric</Cell> <Cell>100</Cell> </MyRow> </TableBody> </Table> <Table aria-label="Table with onAction applied on the rows directly" selectionMode="multiple" > <MyTableHeader> <Column isRowHeader > Name </Column> <Column> Type </Column> <Column> Level </Column> </MyTableHeader> <TableBody> <MyRow onAction={() => alert( `Opening Charizard` )} > <Cell> Charizard </Cell> <Cell> Fire, Flying </Cell> <Cell>67</Cell> </MyRow> <MyRow onAction={() => alert( `Opening Blastoise` )} > <Cell> Blastoise </Cell> <Cell> Water </Cell> <Cell>56</Cell> </MyRow> <MyRow onAction={() => alert( `Opening Venusaur` )} > <Cell> Venusaur </Cell> <Cell> Grass, Poison </Cell> <Cell>83</Cell> </MyRow> <MyRow onAction={() => alert( `Opening Pikachu` )} > <Cell> Pikachu </Cell> <Cell> Electric </Cell> <Cell>100</Cell> </MyRow> </TableBody> </Table> | | Name | Type | Level | | --- | --- | --- | --- | | | Charizard | Fire, Flying | 67 | | | Blastoise | Water | 56 | | | Venusaur | Grass, Poison | 83 | | | Pikachu | Electric | 100 | ### Links# Table rows may also be links to another page or website. This can be achieved by passing the `href` prop to the `<Row>` component. Links behave the same way as described above for row actions depending on the `selectionMode` and `selectionBehavior`. <Table aria-label="Bookmarks" selectionMode="multiple"> <MyTableHeader> <Column isRowHeader>Name</Column> <Column>URL</Column> <Column>Date added</Column> </MyTableHeader> <TableBody> <MyRow href="https://adobe.com/" target="_blank"> <Cell>Adobe</Cell> <Cell>https://adobe.com/</Cell> <Cell>January 28, 2023</Cell> </MyRow> <MyRow href="https://google.com/" target="_blank"> <Cell>Google</Cell> <Cell>https://google.com/</Cell> <Cell>April 5, 2023</Cell> </MyRow> <MyRow href="https://nytimes.com/" target="_blank"> <Cell>New York Times</Cell> <Cell>https://nytimes.com/</Cell> <Cell>July 12, 2023</Cell> </MyRow> </TableBody> </Table> <Table aria-label="Bookmarks" selectionMode="multiple"> <MyTableHeader> <Column isRowHeader>Name</Column> <Column>URL</Column> <Column>Date added</Column> </MyTableHeader> <TableBody> <MyRow href="https://adobe.com/" target="_blank"> <Cell>Adobe</Cell> <Cell>https://adobe.com/</Cell> <Cell>January 28, 2023</Cell> </MyRow> <MyRow href="https://google.com/" target="_blank"> <Cell>Google</Cell> <Cell>https://google.com/</Cell> <Cell>April 5, 2023</Cell> </MyRow> <MyRow href="https://nytimes.com/" target="_blank"> <Cell>New York Times</Cell> <Cell>https://nytimes.com/</Cell> <Cell>July 12, 2023</Cell> </MyRow> </TableBody> </Table> <Table aria-label="Bookmarks" selectionMode="multiple" > <MyTableHeader> <Column isRowHeader > Name </Column> <Column> URL </Column> <Column> Date added </Column> </MyTableHeader> <TableBody> <MyRow href="https://adobe.com/" target="_blank" > <Cell> Adobe </Cell> <Cell> https://adobe.com/ </Cell> <Cell> January 28, 2023 </Cell> </MyRow> <MyRow href="https://google.com/" target="_blank" > <Cell> Google </Cell> <Cell> https://google.com/ </Cell> <Cell> April 5, 2023 </Cell> </MyRow> <MyRow href="https://nytimes.com/" target="_blank" > <Cell> New York Times </Cell> <Cell> https://nytimes.com/ </Cell> <Cell> July 12, 2023 </Cell> </MyRow> </TableBody> </Table> | | Name | URL | Date added | | --- | --- | --- | --- | | | Adobe | https://adobe.com/ | January 28, 2023 | | | Google | https://google.com/ | April 5, 2023 | | | New York Times | https://nytimes.com/ | July 12, 2023 | #### Client side routing# The `<Row>` component works with frameworks and client side routers like Next.js and React Router. As with other React Aria components that support links, this works via the `RouterProvider` component at the root of your app. See the client side routing guide to learn how to set this up. ## Disabled rows# * * * A `Row` can be disabled with the `isDisabled` prop. This will disable all interactions on the row, unless the `disabledBehavior` prop on `Table` is used to change this behavior. Note that you are responsible for the styling of disabled rows, however, the selection checkbox will be automatically disabled. <Table aria-label="Table with disabled rows" selectionMode="multiple"> <MyTableHeader> <Column isRowHeader>Name</Column> <Column>Type</Column> <Column>Level</Column> </MyTableHeader> <TableBody> <MyRow> <Cell>Charizard</Cell> <Cell>Fire, Flying</Cell> <Cell>67</Cell> </MyRow> <MyRow isDisabled> <Cell>Venusaur</Cell> <Cell>Grass, Poison</Cell> <Cell>83</Cell> </MyRow> <MyRow> <Cell>Pikachu</Cell> <Cell>Electric</Cell> <Cell>100</Cell> </MyRow> </TableBody> </Table> <Table aria-label="Table with disabled rows" selectionMode="multiple" > <MyTableHeader> <Column isRowHeader>Name</Column> <Column>Type</Column> <Column>Level</Column> </MyTableHeader> <TableBody> <MyRow> <Cell>Charizard</Cell> <Cell>Fire, Flying</Cell> <Cell>67</Cell> </MyRow> <MyRow isDisabled> <Cell>Venusaur</Cell> <Cell>Grass, Poison</Cell> <Cell>83</Cell> </MyRow> <MyRow> <Cell>Pikachu</Cell> <Cell>Electric</Cell> <Cell>100</Cell> </MyRow> </TableBody> </Table> <Table aria-label="Table with disabled rows" selectionMode="multiple" > <MyTableHeader> <Column isRowHeader > Name </Column> <Column> Type </Column> <Column> Level </Column> </MyTableHeader> <TableBody> <MyRow> <Cell> Charizard </Cell> <Cell> Fire, Flying </Cell> <Cell>67</Cell> </MyRow> <MyRow isDisabled> <Cell> Venusaur </Cell> <Cell> Grass, Poison </Cell> <Cell>83</Cell> </MyRow> <MyRow> <Cell> Pikachu </Cell> <Cell> Electric </Cell> <Cell>100</Cell> </MyRow> </TableBody> </Table> | | Name | Type | Level | | --- | --- | --- | --- | | | Charizard | Fire, Flying | 67 | | | Venusaur | Grass, Poison | 83 | | | Pikachu | Electric | 100 | By default, only row selection is disabled. When `disabledBehavior` is set to `all`, all interactions such as focus, dragging, and actions are also disabled. <Table aria-label="Table with disabled rows" selectionMode="multiple" disabledBehavior="all"> <MyTableHeader> <Column isRowHeader>Name</Column> <Column>Type</Column> <Column>Level</Column> </MyTableHeader> <TableBody> <MyRow> <Cell>Charizard</Cell> <Cell>Fire, Flying</Cell> <Cell>67</Cell> </MyRow> <MyRow isDisabled> <Cell>Venusaur</Cell> <Cell>Grass, Poison</Cell> <Cell>83</Cell> </MyRow> <MyRow> <Cell>Pikachu</Cell> <Cell>Electric</Cell> <Cell>100</Cell> </MyRow> </TableBody> </Table> <Table aria-label="Table with disabled rows" selectionMode="multiple" disabledBehavior="all"> <MyTableHeader> <Column isRowHeader>Name</Column> <Column>Type</Column> <Column>Level</Column> </MyTableHeader> <TableBody> <MyRow> <Cell>Charizard</Cell> <Cell>Fire, Flying</Cell> <Cell>67</Cell> </MyRow> <MyRow isDisabled> <Cell>Venusaur</Cell> <Cell>Grass, Poison</Cell> <Cell>83</Cell> </MyRow> <MyRow> <Cell>Pikachu</Cell> <Cell>Electric</Cell> <Cell>100</Cell> </MyRow> </TableBody> </Table> <Table aria-label="Table with disabled rows" selectionMode="multiple" disabledBehavior="all"> <MyTableHeader> <Column isRowHeader > Name </Column> <Column> Type </Column> <Column> Level </Column> </MyTableHeader> <TableBody> <MyRow> <Cell> Charizard </Cell> <Cell> Fire, Flying </Cell> <Cell>67</Cell> </MyRow> <MyRow isDisabled> <Cell> Venusaur </Cell> <Cell> Grass, Poison </Cell> <Cell>83</Cell> </MyRow> <MyRow> <Cell> Pikachu </Cell> <Cell> Electric </Cell> <Cell>100</Cell> </MyRow> </TableBody> </Table> | | Name | Type | Level | | --- | --- | --- | --- | | | Charizard | Fire, Flying | 67 | | | Venusaur | Grass, Poison | 83 | | | Pikachu | Electric | 100 | In dynamic collections, it may be more convenient to use the `disabledKeys` prop at the `Table` level instead of `isDisabled` on individual rows. This accepts a list of row ids that are disabled. A row is considered disabled if its key exists in `disabledKeys` or if it has `isDisabled`. // Using the same table as above <PokemonTable selectionMode="multiple" disabledKeys={[3]} /> // Using the same table as above <PokemonTable selectionMode="multiple" disabledKeys={[3]} /> // Using the same table as above <PokemonTable selectionMode="multiple" disabledKeys={[3]} /> | | Name | Type | Level | | --- | --- | --- | --- | | | Charizard | Fire, Flying | 67 | | | Blastoise | Water | 56 | | | Venusaur | Grass, Poison | 83 | | | Pikachu | Electric | 100 | ## Sorting# * * * Table supports sorting its data when a column header is pressed. To designate that a Column should support sorting, provide it with the `allowsSorting` prop. The Table accepts a `sortDescriptor` prop that defines the current column key to sort by and the sort direction (ascending/descending). When the user presses a sortable column header, the column's key and sort direction is passed into the `onSortChange` callback, allowing you to update the `sortDescriptor` appropriately. This example performs client side sorting by passing a `sort` function to the useAsyncList hook. See the docs for more information on how to perform server side sorting. import {useAsyncList} from 'react-stately'; interface Character { name: string; height: number; mass: number; birth_year: number; } function AsyncSortTable() { let list = useAsyncList<Character>({ async load({ signal }) { let res = await fetch(`https://swapi.py4e.com/api/people/?search`, { signal }); let json = await res.json(); return { items: json.results }; }, async sort({ items, sortDescriptor }) { return { items: items.sort((a, b) => { let first = a[sortDescriptor.column]; let second = b[sortDescriptor.column]; let cmp = (parseInt(first) || first) < (parseInt(second) || second) ? -1 : 1; if (sortDescriptor.direction === 'descending') { cmp *= -1; } return cmp; }) }; } }); return ( <Table aria-label="Example table with client side sorting" sortDescriptor={list.sortDescriptor} onSortChange={list.sort} > <TableHeader> <MyColumn id="name" isRowHeader allowsSorting>Name</MyColumn> <MyColumn id="height" allowsSorting>Height</MyColumn> <MyColumn id="mass" allowsSorting>Mass</MyColumn> <MyColumn id="birth_year" allowsSorting>Birth Year</MyColumn> </TableHeader> <TableBody items={list.items}> {(item) => ( <Row id={item.name}> <Cell>{item.name}</Cell> <Cell>{item.height}</Cell> <Cell>{item.mass}</Cell> <Cell>{item.birth_year}</Cell> </Row> )} </TableBody> </Table> ); } import {useAsyncList} from 'react-stately'; interface Character { name: string; height: number; mass: number; birth_year: number; } function AsyncSortTable() { let list = useAsyncList<Character>({ async load({ signal }) { let res = await fetch( `https://swapi.py4e.com/api/people/?search`, { signal } ); let json = await res.json(); return { items: json.results }; }, async sort({ items, sortDescriptor }) { return { items: items.sort((a, b) => { let first = a[sortDescriptor.column]; let second = b[sortDescriptor.column]; let cmp = (parseInt(first) || first) < (parseInt(second) || second) ? -1 : 1; if (sortDescriptor.direction === 'descending') { cmp *= -1; } return cmp; }) }; } }); return ( <Table aria-label="Example table with client side sorting" sortDescriptor={list.sortDescriptor} onSortChange={list.sort} > <TableHeader> <MyColumn id="name" isRowHeader allowsSorting> Name </MyColumn> <MyColumn id="height" allowsSorting> Height </MyColumn> <MyColumn id="mass" allowsSorting>Mass</MyColumn> <MyColumn id="birth_year" allowsSorting> Birth Year </MyColumn> </TableHeader> <TableBody items={list.items}> {(item) => ( <Row id={item.name}> <Cell>{item.name}</Cell> <Cell>{item.height}</Cell> <Cell>{item.mass}</Cell> <Cell>{item.birth_year}</Cell> </Row> )} </TableBody> </Table> ); } import {useAsyncList} from 'react-stately'; interface Character { name: string; height: number; mass: number; birth_year: number; } function AsyncSortTable() { let list = useAsyncList< Character >({ async load( { signal } ) { let res = await fetch( `https://swapi.py4e.com/api/people/?search`, { signal } ); let json = await res .json(); return { items: json.results }; }, async sort( { items, sortDescriptor } ) { return { items: items .sort( (a, b) => { let first = a[ sortDescriptor .column ]; let second = b[ sortDescriptor .column ]; let cmp = (parseInt( first ) || first) < (parseInt( second ) || second) ? -1 : 1; if ( sortDescriptor .direction === 'descending' ) { cmp *= -1; } return cmp; } ) }; } }); return ( <Table aria-label="Example table with client side sorting" sortDescriptor={list .sortDescriptor} onSortChange={list .sort} > <TableHeader> <MyColumn id="name" isRowHeader allowsSorting > Name </MyColumn> <MyColumn id="height" allowsSorting > Height </MyColumn> <MyColumn id="mass" allowsSorting > Mass </MyColumn> <MyColumn id="birth_year" allowsSorting > Birth Year </MyColumn> </TableHeader> <TableBody items={list .items} > {(item) => ( <Row id={item .name} > <Cell> {item.name} </Cell> <Cell> {item .height} </Cell> <Cell> {item.mass} </Cell> <Cell> {item .birth_year} </Cell> </Row> )} </TableBody> </Table> ); } | Name▼ | Height▼ | Mass▼ | Birth Year▼ | | --- | --- | --- | --- | | Luke Skywalker | 172 | 77 | 19BBY | | C-3PO | 167 | 75 | 112BBY | | R2-D2 | 96 | 32 | 33BBY | | Darth Vader | 202 | 136 | 41.9BBY | | Leia Organa | 150 | 49 | 19BBY | | Owen Lars | 178 | 120 | 52BBY | | Beru Whitesun lars | 165 | 75 | 47BBY | | R5-D4 | 97 | 32 | unknown | | Biggs Darklighter | 183 | 84 | 24BBY | | Obi-Wan Kenobi | 182 | 77 | 57BBY | Show CSS .react-aria-Column { .sort-indicator { padding: 0 2px; } &:not([data-sort-direction]) .sort-indicator { visibility: hidden; } } .react-aria-Column { .sort-indicator { padding: 0 2px; } &:not([data-sort-direction]) .sort-indicator { visibility: hidden; } } .react-aria-Column { .sort-indicator { padding: 0 2px; } &:not([data-sort-direction]) .sort-indicator { visibility: hidden; } } ## Empty state# * * * Use the `renderEmptyState` prop to customize what the `TableBody` will display if there are no items. <Table aria-label="Search results"> <TableHeader> <Column isRowHeader>Name</Column> <Column>Type</Column> <Column>Date Modified</Column> </TableHeader> <TableBody renderEmptyState={() => 'No results found.'}> {[]} </TableBody> </Table> <Table aria-label="Search results"> <TableHeader> <Column isRowHeader>Name</Column> <Column>Type</Column> <Column>Date Modified</Column> </TableHeader> <TableBody renderEmptyState={() => 'No results found.'}> {[]} </TableBody> </Table> <Table aria-label="Search results"> <TableHeader> <Column isRowHeader > Name </Column> <Column> Type </Column> <Column> Date Modified </Column> </TableHeader> <TableBody renderEmptyState={() => 'No results found.'} > {[]} </TableBody> </Table> | Name | Type | Date Modified | | --- | --- | --- | | No results found. | Show CSS .react-aria-TableBody { &[data-empty] { text-align: center; font-style: italic; } } .react-aria-TableBody { &[data-empty] { text-align: center; font-style: italic; } } .react-aria-TableBody { &[data-empty] { text-align: center; font-style: italic; } } ## Column Resizing# * * * Table supports resizable columns, allowing users to dynamically adjust the width of a column. This is enabled by wrapping the `<Table>` with a `<ResizableTableContainer>` element, which serves as a scrollable container for the Table. Then, to make a column resizable, render a `<ColumnResizer>` element inside a `<Column>`. This allows a user to drag a resize handle to change the width of a column. Keyboard users can also resize a column by pressing Enter to enter resizing mode and then using the arrow keys to resize. Screen reader users can resize columns by operating the resizer like a slider. ### Width values# By default, a Table relies on the browser's default table layout algorithm to determine column widths. However, when a Table is wrapped in a `<ResizableTableContainer>`, column widths are calculated in JavaScript by React Aria. When no additional props are provided, Table divides the available space evenly among the columns. The `Column` component also supports four different width props that allow you to control column sizing behavior: `defaultWidth`, `width`, `minWidth`, and `maxWidth`. An initial, uncontrolled width can be provided to a Column using the `defaultWidth` prop. This allows the column width to freely shrink and grow in relation to other column widths. Alternatively, a controlled value can be provided by the `width` prop. These props accept fixed pixel values, percentages of the total table width, or fractional values (the `fr` unit), which represent a fraction of the available space. Columns without a defined width are equivalent to `1fr`. The `minWidth` and `maxWidth` props define constraints on the size of a column, which may be defined either as fixed pixel values or as percentages of the total table width. These are respected when calculating the size of a column, and also provide limits for column resizing. The example below illustrates how each of the column width props affects their respective column's resize behavior. Note that the column names are wrapped in a `<span tabIndex={-1}>` so that they can be focused with the keyboard in addition to the `<ColumnResizer>`. import {ResizableTableContainer, ColumnResizer} from 'react-aria-components'; <ResizableTableContainer> <Table aria-label="Table with resizable columns"> <TableHeader> <Column id="file" isRowHeader maxWidth={500}> <div className="flex-wrapper"> <span tabIndex={-1} className="column-name">File Name</span> <ColumnResizer /> </div> </Column> <Column id="size" width={80}>Size</Column> <Column id="date" minWidth={100}> <div className="flex-wrapper"> <span tabIndex={-1} className="column-name">Date Modified</span> <ColumnResizer /> </div> </Column> </TableHeader> <TableBody> <Row> <Cell>2022-Roadmap-Proposal-Revision-012822-Copy(2)</Cell> <Cell>214 KB</Cell> <Cell>November 27, 2022 at 4:56PM</Cell> </Row> <Row> <Cell>62259692_p0_master1200</Cell> <Cell>120 KB</Cell> <Cell>January 27, 2021 at 1:56AM</Cell> </Row> </TableBody> </Table> </ResizableTableContainer> import { ColumnResizer, ResizableTableContainer } from 'react-aria-components'; <ResizableTableContainer> <Table aria-label="Table with resizable columns"> <TableHeader> <Column id="file" isRowHeader maxWidth={500}> <div className="flex-wrapper"> <span tabIndex={-1} className="column-name"> File Name </span> <ColumnResizer /> </div> </Column> <Column id="size" width={80}>Size</Column> <Column id="date" minWidth={100}> <div className="flex-wrapper"> <span tabIndex={-1} className="column-name"> Date Modified </span> <ColumnResizer /> </div> </Column> </TableHeader> <TableBody> <Row> <Cell> 2022-Roadmap-Proposal-Revision-012822-Copy(2) </Cell> <Cell>214 KB</Cell> <Cell>November 27, 2022 at 4:56PM</Cell> </Row> <Row> <Cell>62259692_p0_master1200</Cell> <Cell>120 KB</Cell> <Cell>January 27, 2021 at 1:56AM</Cell> </Row> </TableBody> </Table> </ResizableTableContainer> import { ColumnResizer, ResizableTableContainer } from 'react-aria-components'; <ResizableTableContainer> <Table aria-label="Table with resizable columns"> <TableHeader> <Column id="file" isRowHeader maxWidth={500} > <div className="flex-wrapper"> <span tabIndex={-1} className="column-name" > File Name </span> <ColumnResizer /> </div> </Column> <Column id="size" width={80} > Size </Column> <Column id="date" minWidth={100} > <div className="flex-wrapper"> <span tabIndex={-1} className="column-name" > Date Modified </span> <ColumnResizer /> </div> </Column> </TableHeader> <TableBody> <Row> <Cell> 2022-Roadmap-Proposal-Revision-012822-Copy(2) </Cell> <Cell> 214 KB </Cell> <Cell> November 27, 2022 at 4:56PM </Cell> </Row> <Row> <Cell> 62259692_p0_master1200 </Cell> <Cell> 120 KB </Cell> <Cell> January 27, 2021 at 1:56AM </Cell> </Row> </TableBody> </Table> </ResizableTableContainer> | File Name | Size | Date Modified | | --- | --- | --- | | 2022-Roadmap-Proposal-Revision-012822-Copy(2) | 214 KB | November 27, 2022 at 4:56PM | | 62259692\_p0\_master1200 | 120 KB | January 27, 2021 at 1:56AM | Show CSS .react-aria-ResizableTableContainer { max-width: 400px; overflow: auto; position: relative; border: 1px solid var(--border-color); border-radius: 6px; background: var(--background-color); .react-aria-Table { border: none; } .flex-wrapper { display: flex; align-items: center; } .column-name, .react-aria-Button { --background-color: var(--overlay-background); flex: 1; font: inherit; text-align: start; color: inherit; overflow: hidden; text-overflow: ellipsis; border-color: transparent; transition: background 200ms; &[data-hovered] { background: var(--highlight-hover); } &[data-pressed] { background: var(--highlight-pressed); box-shadow: none; } &:focus-visible { outline: 2px solid var(--focus-ring-color); } } .react-aria-ColumnResizer { width: 15px; background-color: grey; height: 25px; flex: 0 0 auto; touch-action: none; box-sizing: border-box; border: 5px; border-style: none solid; border-color: transparent; background-clip: content-box; &[data-resizable-direction=both] { cursor: ew-resize; } &[data-resizable-direction=left] { cursor: e-resize; } &[data-resizable-direction=right] { cursor: w-resize; } &[data-focus-visible] { background-color: var(--focus-ring-color); } &[data-resizing] { border-color: var(--focus-ring-color); background-color: transparent; } } .react-aria-Column, .react-aria-Cell { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } } .react-aria-ResizableTableContainer { max-width: 400px; overflow: auto; position: relative; border: 1px solid var(--border-color); border-radius: 6px; background: var(--background-color); .react-aria-Table { border: none; } .flex-wrapper { display: flex; align-items: center; } .column-name, .react-aria-Button { --background-color: var(--overlay-background); flex: 1; font: inherit; text-align: start; color: inherit; overflow: hidden; text-overflow: ellipsis; border-color: transparent; transition: background 200ms; &[data-hovered] { background: var(--highlight-hover); } &[data-pressed] { background: var(--highlight-pressed); box-shadow: none; } &:focus-visible { outline: 2px solid var(--focus-ring-color); } } .react-aria-ColumnResizer { width: 15px; background-color: grey; height: 25px; flex: 0 0 auto; touch-action: none; box-sizing: border-box; border: 5px; border-style: none solid; border-color: transparent; background-clip: content-box; &[data-resizable-direction=both] { cursor: ew-resize; } &[data-resizable-direction=left] { cursor: e-resize; } &[data-resizable-direction=right] { cursor: w-resize; } &[data-focus-visible] { background-color: var(--focus-ring-color); } &[data-resizing] { border-color: var(--focus-ring-color); background-color: transparent; } } .react-aria-Column, .react-aria-Cell { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } } .react-aria-ResizableTableContainer { max-width: 400px; overflow: auto; position: relative; border: 1px solid var(--border-color); border-radius: 6px; background: var(--background-color); .react-aria-Table { border: none; } .flex-wrapper { display: flex; align-items: center; } .column-name, .react-aria-Button { --background-color: var(--overlay-background); flex: 1; font: inherit; text-align: start; color: inherit; overflow: hidden; text-overflow: ellipsis; border-color: transparent; transition: background 200ms; &[data-hovered] { background: var(--highlight-hover); } &[data-pressed] { background: var(--highlight-pressed); box-shadow: none; } &:focus-visible { outline: 2px solid var(--focus-ring-color); } } .react-aria-ColumnResizer { width: 15px; background-color: grey; height: 25px; flex: 0 0 auto; touch-action: none; box-sizing: border-box; border: 5px; border-style: none solid; border-color: transparent; background-clip: content-box; &[data-resizable-direction=both] { cursor: ew-resize; } &[data-resizable-direction=left] { cursor: e-resize; } &[data-resizable-direction=right] { cursor: w-resize; } &[data-focus-visible] { background-color: var(--focus-ring-color); } &[data-resizing] { border-color: var(--focus-ring-color); background-color: transparent; } } .react-aria-Column, .react-aria-Cell { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } } ### Resize events# Table accepts an `onResize` prop which is triggered whenever a column resizer is moved by the user. This can be used in combination with the `width` prop to update a Column's width in a controlled fashion. Table also accepts an `onResizeEnd` prop, which is triggered when the user finishes a column resize operation. Both events receive a Map object containing the widths of every column in the Table. The example below uses `onResize` to update each of the Table's controlled column widths. It also saves the finalized column widths to `localStorage` in `onResizeEnd`, allowing the Table's state to be preserved between page loads and refreshes. let initialColumns = [ { name: 'File Name', id: 'file', width: '1fr' }, { name: 'Size', id: 'size', width: 80 }, { name: 'Date', id: 'date', width: 100 } ]; function ResizableTable() { let [columns, setColumns] = React.useState(() => { let localStorageWidths = localStorage.getItem('table-widths'); if (localStorageWidths) { let widths = JSON.parse(localStorageWidths); return initialColumns.map((col) => ({ ...col, width: widths[col.id] })); } else { return initialColumns; } }); let onResize = (widths) => { setColumns((columns) => columns.map((col) => ({ ...col, width: widths.get(col.id) })) ); }; let onResizeEnd = (widths) => { localStorage.setItem( 'table-widths', JSON.stringify(Object.fromEntries(widths)) ); }; return ( <ResizableTableContainer onResize={onResize} onResizeEnd={onResizeEnd} > <Table aria-label="Table with controlled, resizable columns saved in local storage"> <TableHeader columns={columns}> {(column) => ( <Column isRowHeader={column.id === 'file'} width={column.width}> <div className="flex-wrapper"> <span tabIndex={-1} className="column-name">{column.name}</span> <ColumnResizer /> </div> </Column> )} </TableHeader> <TableBody> <Row> <Cell>2022-Roadmap-Proposal-Revision-012822-Copy(2)</Cell> <Cell>214 KB</Cell> <Cell>November 27, 2022 at 4:56PM</Cell> </Row> <Row> <Cell>62259692_p0_master1200</Cell> <Cell>120 KB</Cell> <Cell>January 27, 2021 at 1:56AM</Cell> </Row> </TableBody> </Table> </ResizableTableContainer> ); } <ResizableTable /> let initialColumns = [ { name: 'File Name', id: 'file', width: '1fr' }, { name: 'Size', id: 'size', width: 80 }, { name: 'Date', id: 'date', width: 100 } ]; function ResizableTable() { let [columns, setColumns] = React.useState(() => { let localStorageWidths = localStorage.getItem( 'table-widths' ); if (localStorageWidths) { let widths = JSON.parse(localStorageWidths); return initialColumns.map((col) => ({ ...col, width: widths[col.id] })); } else { return initialColumns; } }); let onResize = (widths) => { setColumns((columns) => columns.map((col) => ({ ...col, width: widths.get(col.id) })) ); }; let onResizeEnd = (widths) => { localStorage.setItem( 'table-widths', JSON.stringify(Object.fromEntries(widths)) ); }; return ( <ResizableTableContainer onResize={onResize} onResizeEnd={onResizeEnd} > <Table aria-label="Table with controlled, resizable columns saved in local storage"> <TableHeader columns={columns}> {(column) => ( <Column isRowHeader={column.id === 'file'} width={column.width} > <div className="flex-wrapper"> <span tabIndex={-1} className="column-name"> {column.name} </span> <ColumnResizer /> </div> </Column> )} </TableHeader> <TableBody> <Row> <Cell> 2022-Roadmap-Proposal-Revision-012822-Copy(2) </Cell> <Cell>214 KB</Cell> <Cell>November 27, 2022 at 4:56PM</Cell> </Row> <Row> <Cell>62259692_p0_master1200</Cell> <Cell>120 KB</Cell> <Cell>January 27, 2021 at 1:56AM</Cell> </Row> </TableBody> </Table> </ResizableTableContainer> ); } <ResizableTable /> let initialColumns = [ { name: 'File Name', id: 'file', width: '1fr' }, { name: 'Size', id: 'size', width: 80 }, { name: 'Date', id: 'date', width: 100 } ]; function ResizableTable() { let [ columns, setColumns ] = React.useState( () => { let localStorageWidths = localStorage .getItem( 'table-widths' ); if ( localStorageWidths ) { let widths = JSON .parse( localStorageWidths ); return initialColumns .map( (col) => ({ ...col, width: widths[ col.id ] }) ); } else { return initialColumns; } } ); let onResize = ( widths ) => { setColumns( (columns) => columns.map( (col) => ({ ...col, width: widths .get( col.id ) }) ) ); }; let onResizeEnd = ( widths ) => { localStorage.setItem( 'table-widths', JSON.stringify( Object .fromEntries( widths ) ) ); }; return ( <ResizableTableContainer onResize={onResize} onResizeEnd={onResizeEnd} > <Table aria-label="Table with controlled, resizable columns saved in local storage"> <TableHeader columns={columns} > {(column) => ( <Column isRowHeader={column .id === 'file'} width={column .width} > <div className="flex-wrapper"> <span tabIndex={-1} className="column-name" > {column .name} </span> <ColumnResizer /> </div> </Column> )} </TableHeader> <TableBody> <Row> <Cell> 2022-Roadmap-Proposal-Revision-012822-Copy(2) </Cell> <Cell> 214 KB </Cell> <Cell> November 27, 2022 at 4:56PM </Cell> </Row> <Row> <Cell> 62259692_p0_master1200 </Cell> <Cell> 120 KB </Cell> <Cell> January 27, 2021 at 1:56AM </Cell> </Row> </TableBody> </Table> </ResizableTableContainer> ); } <ResizableTable /> | File Name | Size | Date | | --- | --- | --- | | 2022-Roadmap-Proposal-Revision-012822-Copy(2) | 214 KB | November 27, 2022 at 4:56PM | | 62259692\_p0\_master1200 | 120 KB | January 27, 2021 at 1:56AM | ### Column header menu# The `Column` component exposes a `startResize` function as part of its render props which allows initiating column resizing programmatically. In addition, sorting can also be performed using the `sort` function. This enables you to create a dropdown menu that the user can use to sort or resize a column, along with any other custom actions you may have. Using a menu to initiate column resizing provides a larger hit area for touch screen users. This example shows how to create a reusable component that wraps `<Column>` to include a menu with sorting and resizing functionality. import {Button, Menu, MenuItem, MenuTrigger, Popover} from 'react-aria-components'; interface ResizableTableColumnProps<T> extends Omit<ColumnProps, 'children'> { children: React.ReactNode; } function ResizableTableColumn<T extends object>( props: ResizableTableColumnProps<T> ) { return ( <Column {...props}> {({ startResize, sort, allowsSorting, sortDirection }) => ( <div className="flex-wrapper"> <MenuTrigger> <Button>{props.children}</Button> <Popover> <Menu onAction={(action) => { if (action === 'sortAscending') { sort('ascending'); } else if (action === 'sortDescending') { sort('descending'); } else if (action === 'resize') { startResize(); } }} > <MenuItem id="sortAscending">Sort Ascending</MenuItem> <MenuItem id="sortDescending">Sort Descending</MenuItem> <MenuItem id="resize">Resize</MenuItem> </Menu> </Popover> </MenuTrigger> {allowsSorting && ( <span aria-hidden="true" className="sort-indicator"> {sortDirection === 'ascending' ? '▲' : '▼'} </span> )} <ColumnResizer /> </div> )} </Column> ); } import { Button, Menu, MenuItem, MenuTrigger, Popover } from 'react-aria-components'; interface ResizableTableColumnProps<T> extends Omit<ColumnProps, 'children'> { children: React.ReactNode; } function ResizableTableColumn<T extends object>( props: ResizableTableColumnProps<T> ) { return ( <Column {...props}> {( { startResize, sort, allowsSorting, sortDirection } ) => ( <div className="flex-wrapper"> <MenuTrigger> <Button>{props.children}</Button> <Popover> <Menu onAction={(action) => { if (action === 'sortAscending') { sort('ascending'); } else if (action === 'sortDescending') { sort('descending'); } else if (action === 'resize') { startResize(); } }} > <MenuItem id="sortAscending"> Sort Ascending </MenuItem> <MenuItem id="sortDescending"> Sort Descending </MenuItem> <MenuItem id="resize">Resize</MenuItem> </Menu> </Popover> </MenuTrigger> {allowsSorting && ( <span aria-hidden="true" className="sort-indicator" > {sortDirection === 'ascending' ? '▲' : '▼'} </span> )} <ColumnResizer /> </div> )} </Column> ); } import { Button, Menu, MenuItem, MenuTrigger, Popover } from 'react-aria-components'; interface ResizableTableColumnProps< T > extends Omit< ColumnProps, 'children' > { children: React.ReactNode; } function ResizableTableColumn< T extends object >( props: ResizableTableColumnProps< T > ) { return ( <Column {...props}> {( { startResize, sort, allowsSorting, sortDirection } ) => ( <div className="flex-wrapper"> <MenuTrigger> <Button> {props .children} </Button> <Popover> <Menu onAction={( action ) => { if ( action === 'sortAscending' ) { sort( 'ascending' ); } else if ( action === 'sortDescending' ) { sort( 'descending' ); } else if ( action === 'resize' ) { startResize(); } }} > <MenuItem id="sortAscending"> Sort Ascending </MenuItem> <MenuItem id="sortDescending"> Sort Descending </MenuItem> <MenuItem id="resize"> Resize </MenuItem> </Menu> </Popover> </MenuTrigger> {allowsSorting && ( <span aria-hidden="true" className="sort-indicator" > {sortDirection === 'ascending' ? '▲' : '▼'} </span> )} <ColumnResizer /> </div> )} </Column> ); } We can now use this component in place of `<Column>` to render a table with support for both resizing and sorting columns using a dropdown menu. import type {SortDescriptor} from 'react-aria-components'; function Example() { let [sortDescriptor, setSortDescriptor] = React.useState<SortDescriptor>({ column: 'file', direction: 'ascending' }); let items = [ // ... ].sort((a, b) => { let d = a[sortDescriptor.column].localeCompare(b[sortDescriptor.column]); return sortDescriptor.direction === 'descending' ? -d : d; }); return ( <ResizableTableContainer> <Table aria-label="Table with resizable columns" sortDescriptor={sortDescriptor} onSortChange={setSortDescriptor} > <TableHeader> <ResizableTableColumn id="file" isRowHeader allowsSorting> File Name </ResizableTableColumn> <ResizableTableColumn id="size" allowsSorting> Size </ResizableTableColumn> <ResizableTableColumn id="date" allowsSorting> Date Modified </ResizableTableColumn> </TableHeader> <TableBody items={items}> {(item) => ( <Row> <Cell>{item.file}</Cell> <Cell>{item.size}</Cell> <Cell>{item.date}</Cell> </Row> )} </TableBody> </Table> </ResizableTableContainer> ); } import type {SortDescriptor} from 'react-aria-components'; function Example() { let [sortDescriptor, setSortDescriptor] = React.useState< SortDescriptor >({ column: 'file', direction: 'ascending' }); let items = [ // ... ].sort((a, b) => { let d = a[sortDescriptor.column].localeCompare( b[sortDescriptor.column] ); return sortDescriptor.direction === 'descending' ? -d : d; }); return ( <ResizableTableContainer> <Table aria-label="Table with resizable columns" sortDescriptor={sortDescriptor} onSortChange={setSortDescriptor} > <TableHeader> <ResizableTableColumn id="file" isRowHeader allowsSorting > File Name </ResizableTableColumn> <ResizableTableColumn id="size" allowsSorting> Size </ResizableTableColumn> <ResizableTableColumn id="date" allowsSorting> Date Modified </ResizableTableColumn> </TableHeader> <TableBody items={items}> {(item) => ( <Row> <Cell>{item.file}</Cell> <Cell>{item.size}</Cell> <Cell>{item.date}</Cell> </Row> )} </TableBody> </Table> </ResizableTableContainer> ); } import type {SortDescriptor} from 'react-aria-components'; function Example() { let [ sortDescriptor, setSortDescriptor ] = React.useState< SortDescriptor >({ column: 'file', direction: 'ascending' }); let items = [ // ... ].sort((a, b) => { let d = a[ sortDescriptor .column ].localeCompare( b[ sortDescriptor .column ] ); return sortDescriptor .direction === 'descending' ? -d : d; }); return ( <ResizableTableContainer> <Table aria-label="Table with resizable columns" sortDescriptor={sortDescriptor} onSortChange={setSortDescriptor} > <TableHeader> <ResizableTableColumn id="file" isRowHeader allowsSorting > File Name </ResizableTableColumn> <ResizableTableColumn id="size" allowsSorting > Size </ResizableTableColumn> <ResizableTableColumn id="date" allowsSorting > Date Modified </ResizableTableColumn> </TableHeader> <TableBody items={items} > {(item) => ( <Row> <Cell> {item .file} </Cell> <Cell> {item .size} </Cell> <Cell> {item .date} </Cell> </Row> )} </TableBody> </Table> </ResizableTableContainer> ); } | File Name▲ | Size▼ | Date Modified▼ | | --- | --- | --- | | 2022-Roadmap-Proposal-Revision-012822-Copy(2) | 214 KB | November 27, 2022 at 4:56PM | | 62259692\_p0\_master1200 | 120 KB | January 27, 2021 at 1:56AM | ## Drag and drop# * * * Table supports drag and drop interactions when the `dragAndDropHooks` prop is provided using the `useDragAndDrop` hook. Users can drop data on the table as a whole, on individual rows, insert new items between existing rows, or reorder rows. React Aria supports traditional mouse and touch based drag and drop, but also implements keyboard and screen reader friendly interactions. Users can press Enter on a draggable element to enter drag and drop mode. Then, they can press Tab to navigate between drop targets. A table is treated as a single drop target, so that users can easily tab past it to get to the next drop target. Within a table, keys such as ArrowDown and ArrowUp can be used to select a _drop position_, such as on an row, or between rows. Draggable rows must include a focusable drag handle using a `<Button slot="drag">`. This enables keyboard and screen reader users to initiate drag and drop. The `MyRow` component defined in the reusable wrappers section above includes this as an extra column automatically when the table allows dragging. See the drag and drop introduction to learn more. ### Reorderable# This example shows a basic table that allows users to reorder rows via drag and drop. This is enabled using the `onReorder` event handler, provided to the `useDragAndDrop` hook. The `getItems` function must also be implemented for items to become draggable. See below for more details. This uses useListData from React Stately to manage the item list. Note that `useListData` is a convenience hook, not a requirement. You can manage your state however you wish. import {useListData} from 'react-stately'; import {useDragAndDrop, Button} from 'react-aria-components'; function Example() { let list = useListData({ initialItems: [ {id: 1, name: 'Games', date: '6/7/2020', type: 'File folder'}, {id: 2, name: 'Program Files', date: '4/7/2021', type: 'File folder'}, {id: 3, name: 'bootmgr', date: '11/20/2010', type: 'System file'}, {id: 4, name: 'log.txt', date: '1/18/2016', type: 'Text Document'} ] }); let {dragAndDropHooks} = useDragAndDrop({ getItems: (keys) => [...keys].map(key => ({ 'text/plain': list.getItem(key).name })), onReorder(e) { if (e.target.dropPosition === 'before') { list.moveBefore(e.target.key, e.keys); } else if (e.target.dropPosition === 'after') { list.moveAfter(e.target.key, e.keys); } } }); return ( <Table aria-label="Files" selectionMode="multiple" dragAndDropHooks={dragAndDropHooks} > <TableHeader> <Column></Column> <Column><MyCheckbox slot="selection" /></Column> <Column isRowHeader>Name</Column> <Column>Type</Column> <Column>Date Modified</Column> </TableHeader> <TableBody items={list.items}> {item => ( <Row> <Cell><Button slot="drag">≡</Button></Cell> <Cell><MyCheckbox slot="selection" /></Cell> <Cell>{item.name}</Cell> <Cell>{item.type}</Cell> <Cell>{item.date}</Cell> </Row> )} </TableBody> </Table> ); } import {useListData} from 'react-stately'; import { Button, useDragAndDrop } from 'react-aria-components'; function Example() { let list = useListData({ initialItems: [ { id: 1, name: 'Games', date: '6/7/2020', type: 'File folder' }, { id: 2, name: 'Program Files', date: '4/7/2021', type: 'File folder' }, { id: 3, name: 'bootmgr', date: '11/20/2010', type: 'System file' }, { id: 4, name: 'log.txt', date: '1/18/2016', type: 'Text Document' } ] }); let { dragAndDropHooks } = useDragAndDrop({ getItems: (keys) => [...keys].map((key) => ({ 'text/plain': list.getItem(key).name })), onReorder(e) { if (e.target.dropPosition === 'before') { list.moveBefore(e.target.key, e.keys); } else if (e.target.dropPosition === 'after') { list.moveAfter(e.target.key, e.keys); } } }); return ( <Table aria-label="Files" selectionMode="multiple" dragAndDropHooks={dragAndDropHooks} > <TableHeader> <Column></Column> <Column> <MyCheckbox slot="selection" /> </Column> <Column isRowHeader>Name</Column> <Column>Type</Column> <Column>Date Modified</Column> </TableHeader> <TableBody items={list.items}> {(item) => ( <Row> <Cell> <Button slot="drag">≡</Button> </Cell> <Cell> <MyCheckbox slot="selection" /> </Cell> <Cell>{item.name}</Cell> <Cell>{item.type}</Cell> <Cell>{item.date}</Cell> </Row> )} </TableBody> </Table> ); } import {useListData} from 'react-stately'; import { Button, useDragAndDrop } from 'react-aria-components'; function Example() { let list = useListData( { initialItems: [ { id: 1, name: 'Games', date: '6/7/2020', type: 'File folder' }, { id: 2, name: 'Program Files', date: '4/7/2021', type: 'File folder' }, { id: 3, name: 'bootmgr', date: '11/20/2010', type: 'System file' }, { id: 4, name: 'log.txt', date: '1/18/2016', type: 'Text Document' } ] } ); let { dragAndDropHooks } = useDragAndDrop({ getItems: (keys) => [...keys].map( (key) => ({ 'text/plain': list.getItem( key ).name }) ), onReorder(e) { if ( e.target .dropPosition === 'before' ) { list.moveBefore( e.target.key, e.keys ); } else if ( e.target .dropPosition === 'after' ) { list.moveAfter( e.target.key, e.keys ); } } }); return ( <Table aria-label="Files" selectionMode="multiple" dragAndDropHooks={dragAndDropHooks} > <TableHeader> <Column></Column> <Column> <MyCheckbox slot="selection" /> </Column> <Column isRowHeader > Name </Column> <Column> Type </Column> <Column> Date Modified </Column> </TableHeader> <TableBody items={list .items} > {(item) => ( <Row> <Cell> <Button slot="drag"> ≡ </Button> </Cell> <Cell> <MyCheckbox slot="selection" /> </Cell> <Cell> {item.name} </Cell> <Cell> {item.type} </Cell> <Cell> {item.date} </Cell> </Row> )} </TableBody> </Table> ); } | | | Name | Type | Date Modified | | --- | --- | --- | --- | --- | | ≡ | | Games | File folder | 6/7/2020 | | ≡ | | Program Files | File folder | 4/7/2021 | | ≡ | | bootmgr | System file | 11/20/2010 | | ≡ | | log.txt | Text Document | 1/18/2016 | Show CSS .react-aria-Row { &[data-dragging] { opacity: 0.6; transform: translateZ(0); } [slot=drag] { all: unset; width: 15px; text-align: center; &[data-focus-visible] { border-radius: 4px; outline: 2px solid var(--focus-ring-color); } } } .react-aria-DropIndicator[data-drop-target] { outline: 1px solid var(--highlight-background); transform: translateZ(0); } .react-aria-Row { &[data-dragging] { opacity: 0.6; transform: translateZ(0); } [slot=drag] { all: unset; width: 15px; text-align: center; &[data-focus-visible] { border-radius: 4px; outline: 2px solid var(--focus-ring-color); } } } .react-aria-DropIndicator[data-drop-target] { outline: 1px solid var(--highlight-background); transform: translateZ(0); } .react-aria-Row { &[data-dragging] { opacity: 0.6; transform: translateZ(0); } [slot=drag] { all: unset; width: 15px; text-align: center; &[data-focus-visible] { border-radius: 4px; outline: 2px solid var(--focus-ring-color); } } } .react-aria-DropIndicator[data-drop-target] { outline: 1px solid var(--highlight-background); transform: translateZ(0); } ### Custom drag preview# By default, the drag preview shown under the user's pointer or finger is a copy of the original element that started the drag. A custom preview can be rendered by implementing the `renderDragPreview` function, passed to `useDragAndDrop`. This receives the dragged data that was returned by `getItems`, and returns a rendered preview for those items. This example renders a custom drag preview which shows the number of items being dragged. import {useListData} from 'react-stately'; import {useDragAndDrop} from 'react-aria-components'; function Example() { let {dragAndDropHooks} = useDragAndDrop({ // ... renderDragPreview(items) { return ( <div className="drag-preview"> {items[0]['text/plain']} <span className="badge">{items.length}</span> </div> ); } }); // ... } import {useListData} from 'react-stately'; import {useDragAndDrop} from 'react-aria-components'; function Example() { let {dragAndDropHooks} = useDragAndDrop({ // ... renderDragPreview(items) { return ( <div className="drag-preview"> {items[0]['text/plain']} <span className="badge">{items.length}</span> </div> ); } }); // ... } import {useListData} from 'react-stately'; import {useDragAndDrop} from 'react-aria-components'; function Example() { let { dragAndDropHooks } = useDragAndDrop({ // ... renderDragPreview( items ) { return ( <div className="drag-preview"> {items[0][ 'text/plain' ]} <span className="badge"> {items .length} </span> </div> ); } }); // ... } | | | Name | Type | Date Modified | | --- | --- | --- | --- | --- | | ≡ | | Games | File folder | 6/7/2020 | | ≡ | | Program Files | File folder | 4/7/2021 | | ≡ | | bootmgr | System file | 11/20/2010 | | ≡ | | log.txt | Text Document | 1/18/2016 | Show CSS .drag-preview { width: 150px; padding: 4px 8px; display: flex; align-items: center; justify-content: space-between; gap: 4px; background: var(--highlight-background); color: white; border-radius: 4px; .badge { background: var(--highlight-foreground); color: var(--highlight-background); padding: 0 8px; border-radius: 4px; } } .drag-preview { width: 150px; padding: 4px 8px; display: flex; align-items: center; justify-content: space-between; gap: 4px; background: var(--highlight-background); color: white; border-radius: 4px; .badge { background: var(--highlight-foreground); color: var(--highlight-background); padding: 0 8px; border-radius: 4px; } } .drag-preview { width: 150px; padding: 4px 8px; display: flex; align-items: center; justify-content: space-between; gap: 4px; background: var(--highlight-background); color: white; border-radius: 4px; .badge { background: var(--highlight-foreground); color: var(--highlight-background); padding: 0 8px; border-radius: 4px; } } ### Drag data# Data for draggable items can be provided in multiple formats at once. This allows drop targets to choose data in a format that they understand. For example, you could serialize a complex object as JSON in a custom format for use within your own application, and also provide plain text and/or rich HTML fallbacks that can be used when a user drops data in an external application (e.g. an email message). This can be done by returning multiple keys for an item from the `getItems` function. Types can either be a standard mime type for interoperability with external applications, or a custom string for use within your own app. This example provides representations of each item as plain text, HTML, and a custom app-specific data format. Dropping on the drop targets in this page will use the custom data format to render formatted items. If you drop in an external application supporting rich text, the HTML representation will be used. Dropping in a text editor will use the plain text format. function DraggableTable() { let items = [ {id: 1, name: 'Charizard', type: 'Fire, Flying', level: '67'}, {id: 2, name: 'Blastoise', type: 'Water', level: '56'}, {id: 3, name: 'Venusaur', type: 'Grass, Poison', level: '83'}, {id: 4, name: 'Pikachu', type: 'Electric', level: '100'} ]; let { dragAndDropHooks } = useDragAndDrop({ getItems(keys) { return [...keys].map(key => { let item = items.find(item => item.id === key)!; return { 'text/plain': `${item.name} – ${item.type}`, 'text/html': `<strong>${item.name}</strong> – <em>${item.type}</em>`, 'pokemon': JSON.stringify(item) }; }); }, }); return ( <PokemonTable items={items} selectionMode="multiple" dragAndDropHooks={dragAndDropHooks} /> ); } <div style={{display: 'flex', gap: 12, flexWrap: 'wrap'}}> <DraggableTable /> {/* see below */} <DroppableTable /> </div> function DraggableTable() { let items = [ { id: 1, name: 'Charizard', type: 'Fire, Flying', level: '67' }, { id: 2, name: 'Blastoise', type: 'Water', level: '56' }, { id: 3, name: 'Venusaur', type: 'Grass, Poison', level: '83' }, { id: 4, name: 'Pikachu', type: 'Electric', level: '100' } ]; let { dragAndDropHooks } = useDragAndDrop({ getItems(keys) { return [...keys].map((key) => { let item = items.find((item) => item.id === key)!; return { 'text/plain': `${item.name} – ${item.type}`, 'text/html': `<strong>${item.name}</strong> – <em>${item.type}</em>`, 'pokemon': JSON.stringify(item) }; }); } }); return ( <PokemonTable items={items} selectionMode="multiple" dragAndDropHooks={dragAndDropHooks} /> ); } <div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }} > <DraggableTable /> {/* see below */} <DroppableTable /> </div> function DraggableTable() { let items = [ { id: 1, name: 'Charizard', type: 'Fire, Flying', level: '67' }, { id: 2, name: 'Blastoise', type: 'Water', level: '56' }, { id: 3, name: 'Venusaur', type: 'Grass, Poison', level: '83' }, { id: 4, name: 'Pikachu', type: 'Electric', level: '100' } ]; let { dragAndDropHooks } = useDragAndDrop({ getItems(keys) { return [...keys] .map((key) => { let item = items.find( (item) => item .id === key )!; return { 'text/plain': `${item.name} – ${item.type}`, 'text/html': `<strong>${item.name}</strong> – <em>${item.type}</em>`, 'pokemon': JSON .stringify( item ) }; }); } }); return ( <PokemonTable items={items} selectionMode="multiple" dragAndDropHooks={dragAndDropHooks} /> ); } <div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }} > <DraggableTable /> {/* see below */} <DroppableTable /> </div> | | | Name | Type | Level | | --- | --- | --- | --- | --- | | ≡ | | Charizard | Fire, Flying | 67 | | ≡ | | Blastoise | Water | 56 | | ≡ | | Venusaur | Grass, Poison | 83 | | ≡ | | Pikachu | Electric | 100 | | Name | Type | Level | | --- | --- | --- | | Drop items here | ### Dropping on the collection# Dropping on the Table as a whole can be enabled using the `onRootDrop` event. When a valid drag hovers over the Table, it receives the `isDropTarget` state and can be styled using the `[data-drop-target]` CSS selector. import {isTextDropItem} from 'react-aria-components'; function Example() { let [items, setItems] = React.useState<Pokemon[]>([]); let { dragAndDropHooks } = useDragAndDrop({ async onRootDrop(e) { let items = await Promise.all( e.items .filter(isTextDropItem) .map(async item => ( JSON.parse(await item.getText('pokemon')) ))); setItems(items); } }); return ( <div style={{display: 'flex', gap: 12, flexWrap: 'wrap'}}> <DraggableTable /> <PokemonTable items={items} dragAndDropHooks={dragAndDropHooks} renderEmptyState={() => 'Drop items here'} /> </div> ); } import {isTextDropItem} from 'react-aria-components'; function Example() { let [items, setItems] = React.useState<Pokemon[]>([]); let { dragAndDropHooks } = useDragAndDrop({ async onRootDrop(e) { let items = await Promise.all( e.items .filter(isTextDropItem) .map(async (item) => ( JSON.parse(await item.getText('pokemon')) )) ); setItems(items); } }); return ( <div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }} > <DraggableTable /> <PokemonTable items={items} dragAndDropHooks={dragAndDropHooks} renderEmptyState={() => 'Drop items here'} /> </div> ); } import {isTextDropItem} from 'react-aria-components'; function Example() { let [items, setItems] = React.useState< Pokemon[] >([]); let { dragAndDropHooks } = useDragAndDrop({ async onRootDrop(e) { let items = await Promise .all( e.items .filter( isTextDropItem ) .map( async (item) => ( JSON .parse( await item .getText( 'pokemon' ) ) ) ) ); setItems(items); } }); return ( <div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }} > <DraggableTable /> <PokemonTable items={items} dragAndDropHooks={dragAndDropHooks} renderEmptyState={() => 'Drop items here'} /> </div> ); } | | | Name | Type | Level | | --- | --- | --- | --- | --- | | ≡ | | Charizard | Fire, Flying | 67 | | ≡ | | Blastoise | Water | 56 | | ≡ | | Venusaur | Grass, Poison | 83 | | ≡ | | Pikachu | Electric | 100 | | Name | Type | Level | | --- | --- | --- | | Drop items here | Show CSS .react-aria-Table[data-drop-target] { outline: 2px solid var(--highlight-background); outline-offset: -1px; background: var(--highlight-overlay) } .react-aria-Table[data-drop-target] { outline: 2px solid var(--highlight-background); outline-offset: -1px; background: var(--highlight-overlay) } .react-aria-Table[data-drop-target] { outline: 2px solid var(--highlight-background); outline-offset: -1px; background: var(--highlight-overlay) } ### Dropping on items# Dropping on items can be enabled using the `onItemDrop` event. When a valid drag hovers over an item, it receives the `isDropTarget` state and can be styled using the `[data-drop-target]` CSS selector. function Example() { let { dragAndDropHooks } = useDragAndDrop({ onItemDrop(e) { alert(`Dropped on ${e.target.key}`); } }); return ( <div style={{display: 'flex', gap: 12, flexWrap: 'wrap'}}> {/* see above */} <DraggableTable /> <FileTable dragAndDropHooks={dragAndDropHooks} /> </div> ); } function Example() { let { dragAndDropHooks } = useDragAndDrop({ onItemDrop(e) { alert(`Dropped on ${e.target.key}`); } }); return ( <div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }} > {/* see above */} <DraggableTable /> <FileTable dragAndDropHooks={dragAndDropHooks} /> </div> ); } function Example() { let { dragAndDropHooks } = useDragAndDrop({ onItemDrop(e) { alert( `Dropped on ${e.target.key}` ); } }); return ( <div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }} > {/* see above */} <DraggableTable /> <FileTable dragAndDropHooks={dragAndDropHooks} /> </div> ); } | | | Name | Type | Level | | --- | --- | --- | --- | --- | | ≡ | | Charizard | Fire, Flying | 67 | | ≡ | | Blastoise | Water | 56 | | ≡ | | Venusaur | Grass, Poison | 83 | | ≡ | | Pikachu | Electric | 100 | Type Date Modified | Name | Type | Date Modified | | --- | --- | --- | | Games | File folder | 6/7/2020 | | Program Files | File folder | 4/7/2021 | | bootmgr | System file | 11/20/2010 | | log.txt | Text Document | 1/18/2016 | Add row Show CSS .react-aria-Row[data-drop-target] { outline: 2px solid var(--highlight-background); background: var(--highlight-overlay) } .react-aria-Row[data-drop-target] { outline: 2px solid var(--highlight-background); background: var(--highlight-overlay) } .react-aria-Row[data-drop-target] { outline: 2px solid var(--highlight-background); background: var(--highlight-overlay) } ### Dropping between items# Dropping between items can be enabled using the `onInsert` event. Table renders a `DropIndicator` between items to indicate the insertion position, which can be styled using the `.react-aria-DropIndicator` selector. When it is active, it receives the `[data-drop-target]` state. import {isTextDropItem} from 'react-aria-components'; function Example() { let list = useListData({ initialItems: [ { id: 1, name: 'Bulbasaur', type: 'Grass, Poison', level: '65' }, { id: 2, name: 'Charmander', type: 'Fire', level: '89' }, { id: 3, name: 'Squirtle', type: 'Water', level: '77' }, { id: 4, name: 'Caterpie', type: 'Bug', level: '46' } ] }); let { dragAndDropHooks } = useDragAndDrop({ async onInsert(e) { let items = await Promise.all( e.items.filter(isTextDropItem).map(async (item) => { let { name, type, level } = JSON.parse(await item.getText('pokemon')); return { id: Math.random(), name, type, level }; }) ); if (e.target.dropPosition === 'before') { list.insertBefore(e.target.key, ...items); } else if (e.target.dropPosition === 'after') { list.insertAfter(e.target.key, ...items); } } }); return ( <div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }}> <DraggableTable /> <PokemonTable items={list.items} dragAndDropHooks={dragAndDropHooks} /> </div> ); } import {isTextDropItem} from 'react-aria-components'; function Example() { let list = useListData({ initialItems: [ { id: 1, name: 'Bulbasaur', type: 'Grass, Poison', level: '65' }, { id: 2, name: 'Charmander', type: 'Fire', level: '89' }, { id: 3, name: 'Squirtle', type: 'Water', level: '77' }, { id: 4, name: 'Caterpie', type: 'Bug', level: '46' } ] }); let { dragAndDropHooks } = useDragAndDrop({ async onInsert(e) { let items = await Promise.all( e.items.filter(isTextDropItem).map(async (item) => { let { name, type, level } = JSON.parse( await item.getText('pokemon') ); return { id: Math.random(), name, type, level }; }) ); if (e.target.dropPosition === 'before') { list.insertBefore(e.target.key, ...items); } else if (e.target.dropPosition === 'after') { list.insertAfter(e.target.key, ...items); } } }); return ( <div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }} > <DraggableTable /> <PokemonTable items={list.items} dragAndDropHooks={dragAndDropHooks} /> </div> ); } import {isTextDropItem} from 'react-aria-components'; function Example() { let list = useListData( { initialItems: [ { id: 1, name: 'Bulbasaur', type: 'Grass, Poison', level: '65' }, { id: 2, name: 'Charmander', type: 'Fire', level: '89' }, { id: 3, name: 'Squirtle', type: 'Water', level: '77' }, { id: 4, name: 'Caterpie', type: 'Bug', level: '46' } ] } ); let { dragAndDropHooks } = useDragAndDrop({ async onInsert(e) { let items = await Promise .all( e.items .filter( isTextDropItem ).map( async (item) => { let { name, type, level } = JSON .parse( await item .getText( 'pokemon' ) ); return { id: Math .random(), name, type, level }; } ) ); if ( e.target .dropPosition === 'before' ) { list .insertBefore( e.target.key, ...items ); } else if ( e.target .dropPosition === 'after' ) { list.insertAfter( e.target.key, ...items ); } } }); return ( <div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }} > <DraggableTable /> <PokemonTable items={list .items} dragAndDropHooks={dragAndDropHooks} /> </div> ); } | | | Name | Type | Level | | --- | --- | --- | --- | --- | | ≡ | | Charizard | Fire, Flying | 67 | | ≡ | | Blastoise | Water | 56 | | ≡ | | Venusaur | Grass, Poison | 83 | | ≡ | | Pikachu | Electric | 100 | | Name | Type | Level | | --- | --- | --- | | Bulbasaur | Grass, Poison | 65 | | Charmander | Fire | 89 | | Squirtle | Water | 77 | | Caterpie | Bug | 46 | Show CSS .react-aria-DropIndicator[data-drop-target] { outline: 1px solid var(--highlight-background); transform: translateZ(0); } .react-aria-DropIndicator[data-drop-target] { outline: 1px solid var(--highlight-background); transform: translateZ(0); } .react-aria-DropIndicator[data-drop-target] { outline: 1px solid var(--highlight-background); transform: translateZ(0); } A custom drop indicator can also be rendered with the `renderDropIndicator` function. This lets you customize the DOM structure and CSS classes applied to the drop indicator. import {DropIndicator} from 'react-aria-components'; function Example() { let { dragAndDropHooks } = useDragAndDrop({ // ... renderDropIndicator(target) { return ( <DropIndicator target={target} className={({ isDropTarget }) => `my-drop-indicator ${isDropTarget ? 'active' : ''}`} /> ); } }); // ... } import {DropIndicator} from 'react-aria-components'; function Example() { let { dragAndDropHooks } = useDragAndDrop({ // ... renderDropIndicator(target) { return ( <DropIndicator target={target} className={({ isDropTarget }) => `my-drop-indicator ${ isDropTarget ? 'active' : '' }`} /> ); } }); // ... } import {DropIndicator} from 'react-aria-components'; function Example() { let { dragAndDropHooks } = useDragAndDrop({ // ... renderDropIndicator( target ) { return ( <DropIndicator target={target} className={( { isDropTarget } ) => `my-drop-indicator ${ isDropTarget ? 'active' : '' }`} /> ); } }); // ... } | | | Name | Type | Level | | --- | --- | --- | --- | --- | | ≡ | | Charizard | Fire, Flying | 67 | | ≡ | | Blastoise | Water | 56 | | ≡ | | Venusaur | Grass, Poison | 83 | | ≡ | | Pikachu | Electric | 100 | | Name | Type | Level | | --- | --- | --- | | Bulbasaur | Grass, Poison | 65 | | Charmander | Fire | 89 | | Squirtle | Water | 77 | | Caterpie | Bug | 46 | Show CSS .my-drop-indicator.active { outline: 1px solid #e70073; transform: translateZ(0); } .my-drop-indicator.active { outline: 1px solid #e70073; transform: translateZ(0); } .my-drop-indicator.active { outline: 1px solid #e70073; transform: translateZ(0); } ### Drop data# `Table` allows users to drop one or more **drag items**, each of which contains data to be transferred from the drag source to drop target. There are three kinds of drag items: * `text` – represents data inline as a string in one or more formats * `file` – references a file on the user's device * `directory` – references the contents of a directory #### Text# A `TextDropItem` represents textual data in one or more different formats. These may be either standard mime types or custom app-specific formats. Representing data in multiple formats allows drop targets both within and outside an application to choose data in a format that they understand. For example, a complex object may be serialized in a custom format for use within an application, with fallbacks in plain text and/or rich HTML that can be used when a user drops data from an external application. The example below uses the `acceptedDragTypes` prop to accept items that include a custom app-specific type, which is retrieved using the item's `getText` method. The same draggable component as used in the above example is used here, but rather than displaying the plain text representation, the custom format is used instead. When `acceptedDragTypes` is specified, the dropped items are filtered to include only items that include the accepted types. import {isTextDropItem} from 'react-aria-components'; function DroppableTable() { let [items, setItems] = React.useState<Pokemon[]>([]); let { dragAndDropHooks } = useDragAndDrop({ acceptedDragTypes: ['pokemon'], async onRootDrop(e) { let items = await Promise.all( e.items .filter(isTextDropItem) .map(async item => JSON.parse(await item.getText('pokemon'))) ); setItems(items); } }); return ( <PokemonTable items={items} dragAndDropHooks={dragAndDropHooks} renderEmptyState={() => 'Drop items here'} /> ); } <div style={{display: 'flex', gap: 12, flexWrap: 'wrap'}}> {/* see above */} <DraggableTable /> <DroppableTable /> </div> import {isTextDropItem} from 'react-aria-components'; function DroppableTable() { let [items, setItems] = React.useState<Pokemon[]>([]); let { dragAndDropHooks } = useDragAndDrop({ acceptedDragTypes: ['pokemon'], async onRootDrop(e) { let items = await Promise.all( e.items .filter(isTextDropItem) .map(async (item) => JSON.parse(await item.getText('pokemon')) ) ); setItems(items); } }); return ( <PokemonTable items={items} dragAndDropHooks={dragAndDropHooks} renderEmptyState={() => 'Drop items here'} /> ); } <div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }} > {/* see above */} <DraggableTable /> <DroppableTable /> </div> import {isTextDropItem} from 'react-aria-components'; function DroppableTable() { let [items, setItems] = React.useState< Pokemon[] >([]); let { dragAndDropHooks } = useDragAndDrop({ acceptedDragTypes: [ 'pokemon' ], async onRootDrop(e) { let items = await Promise .all( e.items .filter( isTextDropItem ) .map( async (item) => JSON .parse( await item .getText( 'pokemon' ) ) ) ); setItems(items); } }); return ( <PokemonTable items={items} dragAndDropHooks={dragAndDropHooks} renderEmptyState={() => 'Drop items here'} /> ); } <div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }} > {/* see above */} <DraggableTable /> <DroppableTable /> </div> | | | Name | Type | Level | | --- | --- | --- | --- | --- | | ≡ | | Charizard | Fire, Flying | 67 | | ≡ | | Blastoise | Water | 56 | | ≡ | | Venusaur | Grass, Poison | 83 | | ≡ | | Pikachu | Electric | 100 | | Name | Type | Level | | --- | --- | --- | | Drop items here | #### Files# A `FileDropItem` references a file on the user's device. It includes the name and mime type of the file, and methods to read the contents as plain text, or retrieve a native File object which can be attached to form data for uploading. This example accepts JPEG and PNG image files, and renders them by creating a local object URL. When the list is empty, you can drop on the whole collection, and otherwise items can be inserted. import {isFileDropItem} from 'react-aria-components'; interface ImageItem { id: number, url: string, name: string, type: string, lastModified: number } function Example() { let [items, setItems] = React.useState<ImageItem[]>([]); let { dragAndDropHooks } = useDragAndDrop({ acceptedDragTypes: ['image/jpeg', 'image/png'], async onRootDrop(e) { let items = await Promise.all( e.items.filter(isFileDropItem).map(async item => { let file = await item.getFile(); return { id: Math.random(), url: URL.createObjectURL(file), name: item.name, type: file.type, lastModified: file.lastModified }; }) ); setItems(items); } }); return ( <Table aria-label="Droppable table" dragAndDropHooks={dragAndDropHooks}> <TableHeader> <Column>Image</Column> <Column isRowHeader>Name</Column> <Column>Type</Column> <Column>Last Modified</Column> </TableHeader> <TableBody items={items} renderEmptyState={() => 'Drop images here'}> {item => ( <Row> <Cell><img src={item.url} /></Cell> <Cell>{item.name}</Cell> <Cell>{item.type}</Cell> <Cell>{new Date(item.lastModified).toLocaleString()}</Cell> </Row> )} </TableBody> </Table> ); } import {isFileDropItem} from 'react-aria-components'; interface ImageItem { id: number; url: string; name: string; type: string; lastModified: number; } function Example() { let [items, setItems] = React.useState<ImageItem[]>([]); let { dragAndDropHooks } = useDragAndDrop({ acceptedDragTypes: ['image/jpeg', 'image/png'], async onRootDrop(e) { let items = await Promise.all( e.items.filter(isFileDropItem).map(async (item) => { let file = await item.getFile(); return { id: Math.random(), url: URL.createObjectURL(file), name: item.name, type: file.type, lastModified: file.lastModified }; }) ); setItems(items); } }); return ( <Table aria-label="Droppable table" dragAndDropHooks={dragAndDropHooks} > <TableHeader> <Column>Image</Column> <Column isRowHeader>Name</Column> <Column>Type</Column> <Column>Last Modified</Column> </TableHeader> <TableBody items={items} renderEmptyState={() => 'Drop images here'} > {(item) => ( <Row> <Cell> <img src={item.url} /> </Cell> <Cell>{item.name}</Cell> <Cell>{item.type}</Cell> <Cell> {new Date(item.lastModified).toLocaleString()} </Cell> </Row> )} </TableBody> </Table> ); } import {isFileDropItem} from 'react-aria-components'; interface ImageItem { id: number; url: string; name: string; type: string; lastModified: number; } function Example() { let [items, setItems] = React.useState< ImageItem[] >([]); let { dragAndDropHooks } = useDragAndDrop({ acceptedDragTypes: [ 'image/jpeg', 'image/png' ], async onRootDrop(e) { let items = await Promise .all( e.items .filter( isFileDropItem ).map( async (item) => { let file = await item .getFile(); return { id: Math .random(), url: URL .createObjectURL( file ), name: item .name, type: file .type, lastModified: file .lastModified }; } ) ); setItems(items); } }); return ( <Table aria-label="Droppable table" dragAndDropHooks={dragAndDropHooks} > <TableHeader> <Column> Image </Column> <Column isRowHeader > Name </Column> <Column> Type </Column> <Column> Last Modified </Column> </TableHeader> <TableBody items={items} renderEmptyState={() => 'Drop images here'} > {(item) => ( <Row> <Cell> <img src={item .url} /> </Cell> <Cell> {item.name} </Cell> <Cell> {item.type} </Cell> <Cell> {new Date( item .lastModified ).toLocaleString()} </Cell> </Row> )} </TableBody> </Table> ); } | Image | Name | Type | Last Modified | | --- | --- | --- | --- | | Drop images here | Show CSS .react-aria-Cell img { height: 30px; width: 30px; object-fit: cover; display: block; } .react-aria-Cell img { height: 30px; width: 30px; object-fit: cover; display: block; } .react-aria-Cell img { height: 30px; width: 30px; object-fit: cover; display: block; } #### Directories# A `DirectoryDropItem` references the contents of a directory on the user's device. It includes the name of the directory, as well as a method to iterate through the files and folders within the directory. The contents of any folders within the directory can be accessed recursively. The `getEntries` method returns an async iterable object, which can be used in a `for await...of` loop. This provides each item in the directory as either a `FileDropItem` or` DirectoryDropItem `, and you can access the contents of each file as discussed above. This example accepts directory drops over the whole collection, and renders the contents as items in the list. `DIRECTORY_DRAG_TYPE` is imported from `react-aria-components` and included in the `acceptedDragTypes` prop to limit the accepted items to only directories. import File from '@spectrum-icons/workflow/FileTxt'; import Folder from '@spectrum-icons/workflow/Folder'; import {DIRECTORY_DRAG_TYPE, isDirectoryDropItem} from 'react-aria-components'; interface DirItem { name: string, kind: string, type: string } function Example() { let [files, setFiles] = React.useState<DirItem[]>([]); let { dragAndDropHooks } = useDragAndDrop({ acceptedDragTypes: [DIRECTORY_DRAG_TYPE], async onRootDrop(e) { // Read entries in directory and update state with relevant info. let dir = e.items.find(isDirectoryDropItem)!; let files = []; for await (let entry of dir.getEntries()) { files.push({ name: entry.name, kind: entry.kind, type: entry.kind === 'directory' ? 'Directory' : entry.type }); } setFiles(files); } }); return ( <Table aria-label="Droppable table" dragAndDropHooks={dragAndDropHooks}> <TableHeader> <Column>Kind</Column> <Column isRowHeader>Name</Column> <Column>Type</Column> </TableHeader> <TableBody items={files} renderEmptyState={() => 'Drop directory here'}> {item => ( <Row id={item.name}> <Cell>{item.kind === 'directory' ? <Folder /> : <File />}</Cell> <Cell>{item.name}</Cell> <Cell>{item.type}</Cell> </Row> )} </TableBody> </Table> ); } import File from '@spectrum-icons/workflow/FileTxt'; import Folder from '@spectrum-icons/workflow/Folder'; import { DIRECTORY_DRAG_TYPE, isDirectoryDropItem } from 'react-aria-components'; interface DirItem { name: string; kind: string; type: string; } function Example() { let [files, setFiles] = React.useState<DirItem[]>([]); let { dragAndDropHooks } = useDragAndDrop({ acceptedDragTypes: [DIRECTORY_DRAG_TYPE], async onRootDrop(e) { // Read entries in directory and update state with relevant info. let dir = e.items.find(isDirectoryDropItem)!; let files = []; for await (let entry of dir.getEntries()) { files.push({ name: entry.name, kind: entry.kind, type: entry.kind === 'directory' ? 'Directory' : entry.type }); } setFiles(files); } }); return ( <Table aria-label="Droppable table" dragAndDropHooks={dragAndDropHooks} > <TableHeader> <Column>Kind</Column> <Column isRowHeader>Name</Column> <Column>Type</Column> </TableHeader> <TableBody items={files} renderEmptyState={() => 'Drop directory here'} > {(item) => ( <Row id={item.name}> <Cell> {item.kind === 'directory' ? <Folder /> : <File />} </Cell> <Cell>{item.name}</Cell> <Cell>{item.type}</Cell> </Row> )} </TableBody> </Table> ); } import File from '@spectrum-icons/workflow/FileTxt'; import Folder from '@spectrum-icons/workflow/Folder'; import { DIRECTORY_DRAG_TYPE, isDirectoryDropItem } from 'react-aria-components'; interface DirItem { name: string; kind: string; type: string; } function Example() { let [files, setFiles] = React.useState< DirItem[] >([]); let { dragAndDropHooks } = useDragAndDrop({ acceptedDragTypes: [ DIRECTORY_DRAG_TYPE ], async onRootDrop(e) { // Read entries in directory and update state with relevant info. let dir = e.items .find( isDirectoryDropItem )!; let files = []; for await ( let entry of dir .getEntries() ) { files.push({ name: entry.name, kind: entry.kind, type: entry .kind === 'directory' ? 'Directory' : entry .type }); } setFiles(files); } }); return ( <Table aria-label="Droppable table" dragAndDropHooks={dragAndDropHooks} > <TableHeader> <Column> Kind </Column> <Column isRowHeader > Name </Column> <Column> Type </Column> </TableHeader> <TableBody items={files} renderEmptyState={() => 'Drop directory here'} > {(item) => ( <Row id={item .name} > <Cell> {item .kind === 'directory' ? ( <Folder /> ) : ( <File /> )} </Cell> <Cell> {item.name} </Cell> <Cell> {item.type} </Cell> </Row> )} </TableBody> </Table> ); } | Kind | Name | Type | | --- | --- | --- | | Drop directory here | ### Drop operations# A `DropOperation` is an indication of what will happen when dragged data is dropped on a particular drop target. These are: * `move` – indicates that the dragged data will be moved from its source location to the target location. * `copy` – indicates that the dragged data will be copied to the target destination. * `link` – indicates that there will be a relationship established between the source and target locations. * `cancel` – indicates that the drag and drop operation will be canceled, resulting in no changes made to the source or target. Many operating systems display these in the form of a cursor change, e.g. a plus sign to indicate a copy operation. The user may also be able to use a modifier key to choose which drop operation to perform, such as Option or Alt to switch from move to copy. #### onDragEnd# The `onDragEnd` event allows the drag source to respond when a drag that it initiated ends, either because it was dropped or because it was canceled by the user. The `dropOperation` property of the event object indicates the operation that was performed. For example, when data is moved, the UI could be updated to reflect this change by removing the original dragged items. This example removes the dragged items from the UI when a move operation is completed. Try holding the Option or Alt keys to change the operation to copy, and see how the behavior changes. function Example() { let list = useListData({ initialItems: [ {id: 1, name: 'Charizard', type: 'Fire, Flying', level: '67'}, {id: 2, name: 'Blastoise', type: 'Water', level: '56'}, {id: 3, name: 'Venusaur', type: 'Grass, Poison', level: '83'}, {id: 4, name: 'Pikachu', type: 'Electric', level: '100'} ] }); let { dragAndDropHooks } = useDragAndDrop({ // ... onDragEnd(e) { if (e.dropOperation === 'move') { list.remove(...e.keys); } } }); return ( <div style={{display: 'flex', gap: 12, flexWrap: 'wrap'}}> <PokemonTable items={list.items} selectionMode="multiple" dragAndDropHooks={dragAndDropHooks} /> <DroppableTable /> </div> ); } function Example() { let list = useListData({ initialItems: [ { id: 1, name: 'Charizard', type: 'Fire, Flying', level: '67' }, { id: 2, name: 'Blastoise', type: 'Water', level: '56' }, { id: 3, name: 'Venusaur', type: 'Grass, Poison', level: '83' }, { id: 4, name: 'Pikachu', type: 'Electric', level: '100' } ] }); let { dragAndDropHooks } = useDragAndDrop({ // ... onDragEnd(e) { if (e.dropOperation === 'move') { list.remove(...e.keys); } } }); return ( <div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }} > <PokemonTable items={list.items} selectionMode="multiple" dragAndDropHooks={dragAndDropHooks} /> <DroppableTable /> </div> ); } function Example() { let list = useListData( { initialItems: [ { id: 1, name: 'Charizard', type: 'Fire, Flying', level: '67' }, { id: 2, name: 'Blastoise', type: 'Water', level: '56' }, { id: 3, name: 'Venusaur', type: 'Grass, Poison', level: '83' }, { id: 4, name: 'Pikachu', type: 'Electric', level: '100' } ] } ); let { dragAndDropHooks } = useDragAndDrop({ // ... onDragEnd(e) { if ( e.dropOperation === 'move' ) { list.remove( ...e.keys ); } } }); return ( <div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }} > <PokemonTable items={list .items} selectionMode="multiple" dragAndDropHooks={dragAndDropHooks} /> <DroppableTable /> </div> ); } | | | Name | Type | Level | | --- | --- | --- | --- | --- | | ≡ | | Charizard | Fire, Flying | 67 | | ≡ | | Blastoise | Water | 56 | | ≡ | | Venusaur | Grass, Poison | 83 | | ≡ | | Pikachu | Electric | 100 | | Name | Type | Level | | --- | --- | --- | | Drop items here | #### getAllowedDropOperations# The drag source can also control which drop operations are allowed for the data. For example, if moving data is not allowed, and only copying is supported, the `getAllowedDropOperations` function could be implemented to indicate this. When you drag the element below, the cursor now shows the copy affordance by default, and pressing a modifier to switch drop operations results in the drop being canceled. function Example() { // ... let { dragAndDropHooks } = useDragAndDrop({ // ... getAllowedDropOperations: () => ['copy'] }); return ( <div style={{display: 'flex', gap: 12, flexWrap: 'wrap'}}> <PokemonTable items={list.items} selectionMode="multiple" dragAndDropHooks={dragAndDropHooks} /> <DroppableTable /> </div> ); } function Example() { // ... let { dragAndDropHooks } = useDragAndDrop({ // ... getAllowedDropOperations: () => ['copy'] }); return ( <div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }} > <PokemonTable items={list.items} selectionMode="multiple" dragAndDropHooks={dragAndDropHooks} /> <DroppableTable /> </div> ); } function Example() { // ... let { dragAndDropHooks } = useDragAndDrop({ // ... getAllowedDropOperations: () => ['copy'] }); return ( <div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }} > <PokemonTable items={list .items} selectionMode="multiple" dragAndDropHooks={dragAndDropHooks} /> <DroppableTable /> </div> ); } | | | Name | Type | Level | | --- | --- | --- | --- | --- | | ≡ | | Charizard | Fire, Flying | 67 | | ≡ | | Blastoise | Water | 56 | | ≡ | | Venusaur | Grass, Poison | 83 | | ≡ | | Pikachu | Electric | 100 | | Name | Type | Level | | --- | --- | --- | | Drop items here | #### getDropOperation# The `getDropOperation` function passed to `useDragAndDrop` can be used to provide appropriate feedback to the user when a drag hovers over the drop target. This function receives the drop target, set of types contained in the drag, and a list of allowed drop operations as specified by the drag source. It should return one of the drop operations in `allowedOperations`, or a specific drop operation if only that drop operation is supported. It may also return `'cancel'` to reject the drop. If the returned operation is not in `allowedOperations`, then the drop target will act as if `'cancel'` was returned. In the below example, the drop target only supports dropping PNG images. If a PNG is dragged over the target, it will be highlighted and the operating system displays a copy cursor. If another type is dragged over the target, then there is no visual feedback, indicating that a drop is not accepted there. If the user holds a modifier key such as Control while dragging over the drop target in order to change the drop operation, then the drop target does not accept the drop. function Example() { let [items, setItems] = React.useState<ImageItem[]>([]); let { dragAndDropHooks } = useDragAndDrop({ getDropOperation: () => 'copy', acceptedDragTypes: ['image/png'], async onRootDrop(e) { // ... } }); // See "Files" example above... } function Example() { let [items, setItems] = React.useState<ImageItem[]>([]); let { dragAndDropHooks } = useDragAndDrop({ getDropOperation: () => 'copy', acceptedDragTypes: ['image/png'], async onRootDrop(e) { // ... } }); // See "Files" example above... } function Example() { let [items, setItems] = React.useState< ImageItem[] >([]); let { dragAndDropHooks } = useDragAndDrop({ getDropOperation: () => 'copy', acceptedDragTypes: [ 'image/png' ], async onRootDrop(e) { // ... } }); // See "Files" example above... } | Image | Name | Type | Last Modified | | --- | --- | --- | --- | | Drop PNGs here | #### Drop events# Drop events such as `onInsert`, `onItemDrop`, etc. also include the `dropOperation`. This can be used to perform different actions accordingly, for example, when communicating with a backend API. let onItemDrop = async (e) => { let data = JSON.parse(await e.items[0].getText('my-app-file')); switch (e.dropOperation) { case 'move': MyAppFileService.move(data.filePath, props.filePath); break; case 'copy': MyAppFileService.copy(data.filePath, props.filePath); break; case 'link': MyAppFileService.link(data.filePath, props.filePath); break; }}; let onItemDrop = async (e) => { let data = JSON.parse( await e.items[0].getText('my-app-file') ); switch (e.dropOperation) { case 'move': MyAppFileService.move(data.filePath, props.filePath); break; case 'copy': MyAppFileService.copy(data.filePath, props.filePath); break; case 'link': MyAppFileService.link(data.filePath, props.filePath); break; }}; let onItemDrop = async ( e ) => { let data = JSON.parse( await e.items[0] .getText( 'my-app-file' ) ); switch ( e.dropOperation ) { case 'move': MyAppFileService .move( data.filePath, props.filePath ); break; case 'copy': MyAppFileService .copy( data.filePath, props.filePath ); break; case 'link': MyAppFileService .link( data.filePath, props.filePath ); break; }}; ### Drag between tables# This example puts together many of the concepts described above, allowing users to drag items between tables bidirectionally. It also supports reordering items within the same table. When a table is empty, it accepts drops on the whole collection. `getDropOperation` ensures that items are always moved rather than copied, which avoids duplicate items. import {isTextDropItem} from 'react-aria-components'; interface FileItem { id: string, name: string, type: string } interface DndTableProps { initialItems: FileItem[], 'aria-label': string } function DndTable(props: DndTableProps) { let list = useListData({ initialItems: props.initialItems }); let { dragAndDropHooks } = useDragAndDrop({ // Provide drag data in a custom format as well as plain text. getItems(keys) { return [...keys].map((key) => { let item = list.getItem(key); return { 'custom-app-type': JSON.stringify(item), 'text/plain': item.name }; }); }, // Accept drops with the custom format. acceptedDragTypes: ['custom-app-type'], // Ensure items are always moved rather than copied. getDropOperation: () => 'move', // Handle drops between items from other lists. async onInsert(e) { let processedItems = await Promise.all( e.items .filter(isTextDropItem) .map(async item => JSON.parse(await item.getText('custom-app-type'))) ); if (e.target.dropPosition === 'before') { list.insertBefore(e.target.key, ...processedItems); } else if (e.target.dropPosition === 'after') { list.insertAfter(e.target.key, ...processedItems); } }, // Handle drops on the collection when empty. async onRootDrop(e) { let processedItems = await Promise.all( e.items .filter(isTextDropItem) .map(async item => JSON.parse(await item.getText('custom-app-type'))) ); list.append(...processedItems); }, // Handle reordering items within the same list. onReorder(e) { if (e.target.dropPosition === 'before') { list.moveBefore(e.target.key, e.keys); } else if (e.target.dropPosition === 'after') { list.moveAfter(e.target.key, e.keys); } }, // Remove the items from the source list on drop // if they were moved to a different list. onDragEnd(e) { if (e.dropOperation === 'move' && !e.isInternal) { list.remove(...e.keys); } } }); return ( <Table aria-label={props['aria-label']} selectionMode="multiple" selectedKeys={list.selectedKeys} onSelectionChange={list.setSelectedKeys} dragAndDropHooks={dragAndDropHooks}> <TableHeader> <Column /> <Column><MyCheckbox slot="selection" /></Column> <Column>ID</Column> <Column isRowHeader>Name</Column> <Column>Type</Column> </TableHeader> <TableBody items={list.items} renderEmptyState={() => 'Drop items here'}> {item => ( <Row> <Cell><Button slot="drag">≡</Button></Cell> <Cell><MyCheckbox slot="selection" /></Cell> <Cell>{item.id}</Cell> <Cell>{item.name}</Cell> <Cell>{item.type}</Cell> </Row> )} </TableBody> </Table> ); } <div style={{display: 'flex', gap: 12, flexWrap: 'wrap'}}> <DndTable initialItems={[ { id: '1', type: 'file', name: 'Adobe Photoshop' }, { id: '2', type: 'file', name: 'Adobe XD' }, { id: '3', type: 'folder', name: 'Documents' }, { id: '4', type: 'file', name: 'Adobe InDesign' }, { id: '5', type: 'folder', name: 'Utilities' }, { id: '6', type: 'file', name: 'Adobe AfterEffects' } ]} aria-label="First Table" /> <DndTable initialItems={[ { id: '7', type: 'folder', name: 'Pictures' }, { id: '8', type: 'file', name: 'Adobe Fresco' }, { id: '9', type: 'folder', name: 'Apps' }, { id: '10', type: 'file', name: 'Adobe Illustrator' }, { id: '11', type: 'file', name: 'Adobe Lightroom' }, { id: '12', type: 'file', name: 'Adobe Dreamweaver' } ]} aria-label="Second Table" /> </div> import {isTextDropItem} from 'react-aria-components'; interface FileItem { id: string; name: string; type: string; } interface DndTableProps { initialItems: FileItem[]; 'aria-label': string; } function DndTable(props: DndTableProps) { let list = useListData({ initialItems: props.initialItems }); let { dragAndDropHooks } = useDragAndDrop({ // Provide drag data in a custom format as well as plain text. getItems(keys) { return [...keys].map((key) => { let item = list.getItem(key); return { 'custom-app-type': JSON.stringify(item), 'text/plain': item.name }; }); }, // Accept drops with the custom format. acceptedDragTypes: ['custom-app-type'], // Ensure items are always moved rather than copied. getDropOperation: () => 'move', // Handle drops between items from other lists. async onInsert(e) { let processedItems = await Promise.all( e.items .filter(isTextDropItem) .map(async (item) => JSON.parse( await item.getText('custom-app-type') ) ) ); if (e.target.dropPosition === 'before') { list.insertBefore(e.target.key, ...processedItems); } else if (e.target.dropPosition === 'after') { list.insertAfter(e.target.key, ...processedItems); } }, // Handle drops on the collection when empty. async onRootDrop(e) { let processedItems = await Promise.all( e.items .filter(isTextDropItem) .map(async (item) => JSON.parse( await item.getText('custom-app-type') ) ) ); list.append(...processedItems); }, // Handle reordering items within the same list. onReorder(e) { if (e.target.dropPosition === 'before') { list.moveBefore(e.target.key, e.keys); } else if (e.target.dropPosition === 'after') { list.moveAfter(e.target.key, e.keys); } }, // Remove the items from the source list on drop // if they were moved to a different list. onDragEnd(e) { if (e.dropOperation === 'move' && !e.isInternal) { list.remove(...e.keys); } } }); return ( <Table aria-label={props['aria-label']} selectionMode="multiple" selectedKeys={list.selectedKeys} onSelectionChange={list.setSelectedKeys} dragAndDropHooks={dragAndDropHooks} > <TableHeader> <Column /> <Column> <MyCheckbox slot="selection" /> </Column> <Column>ID</Column> <Column isRowHeader>Name</Column> <Column>Type</Column> </TableHeader> <TableBody items={list.items} renderEmptyState={() => 'Drop items here'} > {(item) => ( <Row> <Cell> <Button slot="drag">≡</Button> </Cell> <Cell> <MyCheckbox slot="selection" /> </Cell> <Cell>{item.id}</Cell> <Cell>{item.name}</Cell> <Cell>{item.type}</Cell> </Row> )} </TableBody> </Table> ); } <div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }} > <DndTable initialItems={[ { id: '1', type: 'file', name: 'Adobe Photoshop' }, { id: '2', type: 'file', name: 'Adobe XD' }, { id: '3', type: 'folder', name: 'Documents' }, { id: '4', type: 'file', name: 'Adobe InDesign' }, { id: '5', type: 'folder', name: 'Utilities' }, { id: '6', type: 'file', name: 'Adobe AfterEffects' } ]} aria-label="First Table" /> <DndTable initialItems={[ { id: '7', type: 'folder', name: 'Pictures' }, { id: '8', type: 'file', name: 'Adobe Fresco' }, { id: '9', type: 'folder', name: 'Apps' }, { id: '10', type: 'file', name: 'Adobe Illustrator' }, { id: '11', type: 'file', name: 'Adobe Lightroom' }, { id: '12', type: 'file', name: 'Adobe Dreamweaver' } ]} aria-label="Second Table" /> </div> import {isTextDropItem} from 'react-aria-components'; interface FileItem { id: string; name: string; type: string; } interface DndTableProps { initialItems: FileItem[]; 'aria-label': string; } function DndTable( props: DndTableProps ) { let list = useListData( { initialItems: props .initialItems } ); let { dragAndDropHooks } = useDragAndDrop({ // Provide drag data in a custom format as well as plain text. getItems(keys) { return [...keys] .map((key) => { let item = list .getItem( key ); return { 'custom-app-type': JSON .stringify( item ), 'text/plain': item.name }; }); }, // Accept drops with the custom format. acceptedDragTypes: [ 'custom-app-type' ], // Ensure items are always moved rather than copied. getDropOperation: () => 'move', // Handle drops between items from other lists. async onInsert(e) { let processedItems = await Promise .all( e.items .filter( isTextDropItem ) .map( async (item) => JSON .parse( await item .getText( 'custom-app-type' ) ) ) ); if ( e.target .dropPosition === 'before' ) { list .insertBefore( e.target.key, ...processedItems ); } else if ( e.target .dropPosition === 'after' ) { list.insertAfter( e.target.key, ...processedItems ); } }, // Handle drops on the collection when empty. async onRootDrop(e) { let processedItems = await Promise .all( e.items .filter( isTextDropItem ) .map( async (item) => JSON .parse( await item .getText( 'custom-app-type' ) ) ) ); list.append( ...processedItems ); }, // Handle reordering items within the same list. onReorder(e) { if ( e.target .dropPosition === 'before' ) { list.moveBefore( e.target.key, e.keys ); } else if ( e.target .dropPosition === 'after' ) { list.moveAfter( e.target.key, e.keys ); } }, // Remove the items from the source list on drop // if they were moved to a different list. onDragEnd(e) { if ( e.dropOperation === 'move' && !e.isInternal ) { list.remove( ...e.keys ); } } }); return ( <Table aria-label={props[ 'aria-label' ]} selectionMode="multiple" selectedKeys={list .selectedKeys} onSelectionChange={list .setSelectedKeys} dragAndDropHooks={dragAndDropHooks} > <TableHeader> <Column /> <Column> <MyCheckbox slot="selection" /> </Column> <Column> ID </Column> <Column isRowHeader > Name </Column> <Column> Type </Column> </TableHeader> <TableBody items={list .items} renderEmptyState={() => 'Drop items here'} > {(item) => ( <Row> <Cell> <Button slot="drag"> ≡ </Button> </Cell> <Cell> <MyCheckbox slot="selection" /> </Cell> <Cell> {item.id} </Cell> <Cell> {item.name} </Cell> <Cell> {item.type} </Cell> </Row> )} </TableBody> </Table> ); } <div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }} > <DndTable initialItems={[ { id: '1', type: 'file', name: 'Adobe Photoshop' }, { id: '2', type: 'file', name: 'Adobe XD' }, { id: '3', type: 'folder', name: 'Documents' }, { id: '4', type: 'file', name: 'Adobe InDesign' }, { id: '5', type: 'folder', name: 'Utilities' }, { id: '6', type: 'file', name: 'Adobe AfterEffects' } ]} aria-label="First Table" /> <DndTable initialItems={[ { id: '7', type: 'folder', name: 'Pictures' }, { id: '8', type: 'file', name: 'Adobe Fresco' }, { id: '9', type: 'folder', name: 'Apps' }, { id: '10', type: 'file', name: 'Adobe Illustrator' }, { id: '11', type: 'file', name: 'Adobe Lightroom' }, { id: '12', type: 'file', name: 'Adobe Dreamweaver' } ]} aria-label="Second Table" /> </div> | | | ID | Name | Type | | --- | --- | --- | --- | --- | | ≡ | | 1 | Adobe Photoshop | file | | ≡ | | 2 | Adobe XD | file | | ≡ | | 3 | Documents | folder | | ≡ | | 4 | Adobe InDesign | file | | ≡ | | 5 | Utilities | folder | | ≡ | | 6 | Adobe AfterEffects | file | | | | ID | Name | Type | | --- | --- | --- | --- | --- | | ≡ | | 7 | Pictures | folder | | ≡ | | 8 | Adobe Fresco | file | | ≡ | | 9 | Apps | folder | | ≡ | | 10 | Adobe Illustrator | file | | ≡ | | 11 | Adobe Lightroom | file | | ≡ | | 12 | Adobe Dreamweaver | file | ## Props# * * * ### Table# | Name | Type | Default | Description | | --- | --- | --- | --- | | `children` | `ReactNode` | — | The elements that make up the table. Includes the TableHeader, TableBody, Columns, and Rows. | | `selectionBehavior` | ` SelectionBehavior ` | `"toggle"` | How multiple selection should behave in the collection. | | `disabledBehavior` | ` DisabledBehavior ` | `"selection"` | Whether `disabledKeys` applies to all interactions, or only selection. | | `dragAndDropHooks` | ` DragAndDropHooks ` | — | The drag and drop hooks returned by `useDragAndDrop` used to enable drag and drop behavior for the Table. | | `disabledKeys` | `Iterable<Key>` | — | A list of row keys to disable. | | `escapeKeyBehavior` | `'clearSelection' | 'none'` | `'clearSelection'` | Whether pressing the escape key should clear selection in the table or not. Most experiences should not modify this option as it eliminates a keyboard user's ability to easily clear selection. Only use if the escape key is being handled externally or should not trigger selection clearing contextually. | | `shouldSelectOnPressUp` | `boolean` | — | Whether selection should occur on press up instead of press down. | | `selectionMode` | ` SelectionMode ` | — | The type of selection that is allowed in the collection. | | `disallowEmptySelection` | `boolean` | — | Whether the collection allows empty selection. | | `selectedKeys` | `'all' | Iterable<Key>` | — | The currently selected keys in the collection (controlled). | | `defaultSelectedKeys` | `'all' | Iterable<Key>` | — | The initial selected keys in the collection (uncontrolled). | | `sortDescriptor` | ` SortDescriptor ` | — | The current sorted column and direction. | | `className` | `string | ( (values: TableRenderProps & & { defaultClassName: string | | undefined } )) => string` | — | The CSS className for the element. A function may be provided to compute the class based on component state. | | `style` | `CSSProperties | ( (values: TableRenderProps & & { defaultStyle: CSSProperties } )) => CSSProperties | undefined` | — | The inline style for the element. A function may be provided to compute the style based on component state. | Events | Name | Type | Description | | --- | --- | --- | | `onRowAction` | `( (key: Key )) => void` | Handler that is called when a user performs an action on the row. | | `onSelectionChange` | `( (keys: Selection )) => void` | Handler that is called when the selection changes. | | `onSortChange` | `( (descriptor: SortDescriptor )) => any` | Handler that is called when the sorted column or direction changes. | | `onScroll` | `( (e: UIEvent<Element> )) => void` | Handler that is called when a user scrolls. See MDN. | Layout | Name | Type | Description | | --- | --- | --- | | `slot` | `string | null` | A slot name for the component. Slots allow the component to receive props from a parent component. An explicit `null` value indicates that the local props completely override all props received from a parent. | Accessibility | Name | Type | Description | | --- | --- | --- | | `aria-label` | `string` | Defines a string value that labels the current element. | | `aria-labelledby` | `string` | Identifies the element (or elements) that labels the current element. | | `aria-describedby` | `string` | Identifies the element (or elements) that describes the object. | | `aria-details` | `string` | Identifies the element (or elements) that provide a detailed, extended description for the object. | ### TableHeader# | Name | Type | Description | | --- | --- | --- | | `columns` | `Iterable<object>` | A list of table columns. | | `children` | `ReactNode | ( (item: object )) => ReactElement` | A list of `Column(s)` or a function. If the latter, a list of columns must be provided using the `columns` prop. | | `dependencies` | `ReadonlyArray<any>` | Values that should invalidate the column cache when using dynamic collections. | | `className` | `string | ( (values: TableHeaderRenderProps & & { defaultClassName: string | | undefined } )) => string` | The CSS className for the element. A function may be provided to compute the class based on component state. | | `style` | `CSSProperties | ( (values: TableHeaderRenderProps & & { defaultStyle: CSSProperties } )) => CSSProperties | undefined` | The inline style for the element. A function may be provided to compute the style based on component state. | Events | Name | Type | Description | | --- | --- | --- | | `onHoverStart` | `( (e: HoverEvent )) => void` | Handler that is called when a hover interaction starts. | | `onHoverEnd` | `( (e: HoverEvent )) => void` | Handler that is called when a hover interaction ends. | | `onHoverChange` | `( (isHovering: boolean )) => void` | Handler that is called when the hover state changes. | ### Column# | Name | Type | Description | | --- | --- | --- | | `id` | `Key` | The unique id of the column. | | `allowsSorting` | `boolean` | Whether the column allows sorting. | | `isRowHeader` | `boolean` | Whether a column is a row header and should be announced by assistive technology during row navigation. | | `textValue` | `string` | A string representation of the column's contents, used for accessibility announcements. | | `children` | `ReactNode | ( (values: ColumnRenderProps & & { defaultChildren: ReactNode | | undefined } )) => ReactNode` | The children of the component. A function may be provided to alter the children based on component state. | | `className` | `string | ( (values: ColumnRenderProps & & { defaultClassName: string | | undefined } )) => string` | The CSS className for the element. A function may be provided to compute the class based on component state. | | `style` | `CSSProperties | ( (values: ColumnRenderProps & & { defaultStyle: CSSProperties } )) => CSSProperties | undefined` | The inline style for the element. A function may be provided to compute the style based on component state. | Sizing | Name | Type | Description | | --- | --- | --- | | `width` | ` ColumnSize | null` | The width of the column. This prop only applies when the `<Table>` is wrapped in a `<ResizableTableContainer>`. | | `minWidth` | ` ColumnStaticSize | null` | The minimum width of the column. This prop only applies when the `<Table>` is wrapped in a `<ResizableTableContainer>`. | | `maxWidth` | ` ColumnStaticSize | null` | The maximum width of the column. This prop only applies when the `<Table>` is wrapped in a `<ResizableTableContainer>`. | | `defaultWidth` | ` ColumnSize | null` | The default width of the column. This prop only applies when the `<Table>` is wrapped in a `<ResizableTableContainer>`. | ### TableBody# | Name | Type | Description | | --- | --- | --- | | `renderEmptyState` | `( (props: TableBodyRenderProps )) => ReactNode` | Provides content to display when there are no rows in the table. | | `children` | `ReactNode | ( (item: T )) => ReactNode` | The contents of the collection. | | `dependencies` | `ReadonlyArray<any>` | Values that should invalidate the item cache when using dynamic collections. | | `items` | `Iterable<T>` | Item objects in the collection. | | `className` | `string | ( (values: TableBodyRenderProps & & { defaultClassName: string | | undefined } )) => string` | The CSS className for the element. A function may be provided to compute the class based on component state. | | `style` | `CSSProperties | ( (values: TableBodyRenderProps & & { defaultStyle: CSSProperties } )) => CSSProperties | undefined` | The inline style for the element. A function may be provided to compute the style based on component state. | ### Row# | Name | Type | Description | | --- | --- | --- | | `columns` | `Iterable<object>` | A list of columns used when dynamically rendering cells. | | `children` | `ReactNode | ( (item: object )) => ReactElement` | The cells within the row. Supports static items or a function for dynamic rendering. | | `value` | `object` | The object value that this row represents. When using dynamic collections, this is set automatically. | | `dependencies` | `ReadonlyArray<any>` | Values that should invalidate the cell cache when using dynamic collections. | | `textValue` | `string` | A string representation of the row's contents, used for features like typeahead. | | `isDisabled` | `boolean` | Whether the row is disabled. | | `id` | `Key` | The unique id of the row. | | `className` | `string | ( (values: RowRenderProps & & { defaultClassName: string | | undefined } )) => string` | The CSS className for the element. A function may be provided to compute the class based on component state. | | `style` | `CSSProperties | ( (values: RowRenderProps & & { defaultStyle: CSSProperties } )) => CSSProperties | undefined` | The inline style for the element. A function may be provided to compute the style based on component state. | | `href` | ` Href ` | A URL to link to. See MDN. | | `hrefLang` | `string` | Hints at the human language of the linked URL. SeeMDN. | | `target` | `HTMLAttributeAnchorTarget` | The target window for the link. See MDN. | | `rel` | `string` | The relationship between the linked resource and the current page. See MDN. | | `download` | `boolean | string` | Causes the browser to download the linked URL. A string may be provided to suggest a file name. See MDN. | | `ping` | `string` | A space-separated list of URLs to ping when the link is followed. See MDN. | | `referrerPolicy` | `HTMLAttributeReferrerPolicy` | How much of the referrer to send when following the link. See MDN. | | `routerOptions` | ` RouterOptions ` | Options for the configured client side router. | Events | Name | Type | Description | | --- | --- | --- | | `onAction` | `() => void` | Handler that is called when a user performs an action on the row. The exact user event depends on the collection's `selectionBehavior` prop and the interaction modality. | | `onHoverStart` | `( (e: HoverEvent )) => void` | Handler that is called when a hover interaction starts. | | `onHoverEnd` | `( (e: HoverEvent )) => void` | Handler that is called when a hover interaction ends. | | `onHoverChange` | `( (isHovering: boolean )) => void` | Handler that is called when the hover state changes. | ### Cell# | Name | Type | Description | | --- | --- | --- | | `id` | `Key` | The unique id of the cell. | | `textValue` | `string` | A string representation of the cell's contents, used for features like typeahead. | | `colSpan` | `number` | Indicates how many columns the data cell spans. | | `children` | `ReactNode | ( (values: CellRenderProps & & { defaultChildren: ReactNode | | undefined } )) => ReactNode` | The children of the component. A function may be provided to alter the children based on component state. | | `className` | `string | ( (values: CellRenderProps & & { defaultClassName: string | | undefined } )) => string` | The CSS className for the element. A function may be provided to compute the class based on component state. | | `style` | `CSSProperties | ( (values: CellRenderProps & & { defaultStyle: CSSProperties } )) => CSSProperties | undefined` | The inline style for the element. A function may be provided to compute the style based on component state. | ### ResizableTableContainer# | Name | Type | Description | | --- | --- | --- | | `children` | `ReactNode` | The children of the component. | | `className` | `string` | The CSS className for the element. | | `style` | `CSSProperties` | The inline style for the element. | Events | Name | Type | Description | | --- | --- | --- | | `onResizeStart` | `( (widths: Map<Key, ColumnSize > )) => void` | Handler that is called when a user starts a column resize. | | `onResize` | `( (widths: Map<Key, ColumnSize > )) => void` | Handler that is called when a user performs a column resize. Can be used with the width property on columns to put the column widths into a controlled state. | | `onResizeEnd` | `( (widths: Map<Key, ColumnSize > )) => void` | Handler that is called after a user performs a column resize. Can be used to store the widths of columns for another future session. | | `onScroll` | `( (e: UIEvent<Element> )) => void` | Handler that is called when a user scrolls. See MDN. | Accessibility | Name | Type | Description | | --- | --- | --- | | `id` | `string` | The element's unique identifier. See MDN. | ### ColumnResizer# | Name | Type | Description | | --- | --- | --- | | `children` | `ReactNode | ( (values: ColumnResizerRenderProps & & { defaultChildren: ReactNode | | undefined } )) => ReactNode` | The children of the component. A function may be provided to alter the children based on component state. | | `className` | `string | ( (values: ColumnResizerRenderProps & & { defaultClassName: string | | undefined } )) => string` | The CSS className for the element. A function may be provided to compute the class based on component state. | | `style` | `CSSProperties | ( (values: ColumnResizerRenderProps & & { defaultStyle: CSSProperties } )) => CSSProperties | undefined` | The inline style for the element. A function may be provided to compute the style based on component state. | Events | Name | Type | Description | | --- | --- | --- | | `onHoverStart` | `( (e: HoverEvent )) => void` | Handler that is called when a hover interaction starts. | | `onHoverEnd` | `( (e: HoverEvent )) => void` | Handler that is called when a hover interaction ends. | | `onHoverChange` | `( (isHovering: boolean )) => void` | Handler that is called when the hover state changes. | Accessibility | Name | Type | Description | | --- | --- | --- | | `aria-label` | `string` | A custom accessibility label for the resizer. | ## Styling# * * * React Aria components can be styled in many ways, including using CSS classes, inline styles, utility classes (e.g. Tailwind), CSS-in-JS (e.g. Styled Components), etc. By default, all components include a builtin `className` attribute which can be targeted using CSS selectors. These follow the `react-aria-ComponentName` naming convention. .react-aria-Table { /* ... */ } .react-aria-Table { /* ... */ } .react-aria-Table { /* ... */ } A custom `className` can also be specified on any component. This overrides the default `className` provided by React Aria with your own. <Table className="my-table"> {/* ... */} </Table> <Table className="my-table"> {/* ... */} </Table> <Table className="my-table"> {/* ... */} </Table> In addition, some components support multiple UI states (e.g. pressed, hovered, etc.). React Aria components expose states using data attributes, which you can target in CSS selectors. For example: .react-aria-Row[data-selected] { /* ... */ } .react-aria-Row[data-focused] { /* ... */ } .react-aria-Row[data-selected] { /* ... */ } .react-aria-Row[data-focused] { /* ... */ } .react-aria-Row[data-selected] { /* ... */ } .react-aria-Row[data-focused] { /* ... */ } The `className` and `style` props also accept functions which receive states for styling. This lets you dynamically determine the classes or styles to apply, which is useful when using utility CSS libraries like Tailwind. <Row className={({ isSelected }) => isSelected ? 'bg-blue-400' : 'bg-gray-100'} > {/* ... */} </Row> <Row className={({ isSelected }) => isSelected ? 'bg-blue-400' : 'bg-gray-100'} > {/* ... */} </Row> <Row className={( { isSelected } ) => isSelected ? 'bg-blue-400' : 'bg-gray-100'} > {/* ... */} </Row> 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 sort indicator in sortable columns. <Column> {({allowsSorting, sortDirection}) => ( <> Column Title {allowsSorting && <MySortIndicator direction={sortDirection} />} </> )} </Column> <Column> {({ allowsSorting, sortDirection }) => ( <> Column Title {allowsSorting && ( <MySortIndicator direction={sortDirection} /> )} </> )} </Column> <Column> {( { allowsSorting, sortDirection } ) => ( <> Column Title {allowsSorting && ( <MySortIndicator direction={sortDirection} /> )} </> )} </Column> The states and selectors for each component used in a `Table` are documented below. ### Table# A `Table` can be targeted with the `.react-aria-Table` CSS selector, or by overriding with a custom `className`. It supports the following states and render props: | Name | CSS Selector | Description | | --- | --- | --- | | `isFocused` | `[data-focused]` | Whether the table is currently focused. | | `isFocusVisible` | `[data-focus-visible]` | Whether the table is currently keyboard focused. | | `isDropTarget` | `[data-drop-target]` | Whether the table is currently the active drop target. | | `state` | `—` | State of the table. | ### TableHeader# A `TableHeader` can be targeted with the `.react-aria-TableHeader` CSS selector, or by overriding with a custom `className`. ### Column# A `Column` can be targeted with the `.react-aria-Column` CSS selector, or by overriding with a custom `className`. It supports the following states and render props: | Name | CSS Selector | Description | | --- | --- | --- | | `isHovered` | `[data-hovered]` | Whether the item is currently hovered with a mouse. | | `isFocused` | `[data-focused]` | Whether the item is currently focused. | | `isFocusVisible` | `[data-focus-visible]` | Whether the item is currently keyboard focused. | | `allowsSorting` | `[data-allows-sorting]` | Whether the column allows sorting. | | `sortDirection` | `[data-sort-direction="ascending | descending"]` | The current sort direction. | | `isResizing` | `[data-resizing]` | Whether the column is currently being resized. | | `sort` | `—` | Triggers sorting for this column in the given direction. | | `startResize` | `—` | Starts column resizing if the table is contained in a `<ResizableTableContainer>` element. | ### TableBody# A `TableBody` can be targeted with the `.react-aria-TableBody` CSS selector, or by overriding with a custom `className`. It supports the following states and render props: | Name | CSS Selector | Description | | --- | --- | --- | | `isEmpty` | `[data-empty]` | Whether the table body has no rows and should display its empty state. | | `isDropTarget` | `[data-drop-target]` | Whether the Table is currently the active drop target. | ### Row# A `Row` can be targeted with the `.react-aria-Row` CSS selector, or by overriding with a custom `className`. It supports the following states and render props: | Name | CSS Selector | Description | | --- | --- | --- | | `isFocusVisibleWithin` | `—` | Whether the row's children have keyboard focus. | | `id` | `—` | The unique id of the row. | | `isHovered` | `[data-hovered]` | Whether the item is currently hovered with a mouse. | | `isPressed` | `[data-pressed]` | Whether the item is currently in a pressed state. | | `isSelected` | `[data-selected]` | Whether the item is currently selected. | | `isFocused` | `[data-focused]` | Whether the item is currently focused. | | `isFocusVisible` | `[data-focus-visible]` | Whether the item is currently keyboard focused. | | `isDisabled` | `[data-disabled]` | Whether the item is non-interactive, i.e. both selection and actions are disabled and the item may not be focused. Dependent on `disabledKeys` and `disabledBehavior`. | | `selectionMode` | `[data-selection-mode="single | multiple"]` | The type of selection that is allowed in the collection. | | `selectionBehavior` | `—` | The selection behavior for the collection. | | `allowsDragging` | `[data-allows-dragging]` | Whether the item allows dragging. | | `isDragging` | `[data-dragging]` | Whether the item is currently being dragged. | | `isDropTarget` | `[data-drop-target]` | Whether the item is currently an active drop target. | ### Cell# A `Cell` can be targeted with the `.react-aria-Cell` CSS selector, or by overriding with a custom `className`. It supports the following states and render props: | Name | CSS Selector | Description | | --- | --- | --- | | `isPressed` | `[data-pressed]` | Whether the cell is currently in a pressed state. | | `isFocused` | `[data-focused]` | Whether the cell is currently focused. | | `isFocusVisible` | `[data-focus-visible]` | Whether the cell is currently keyboard focused. | | `isHovered` | `[data-hovered]` | Whether the cell is currently hovered with a mouse. | ### ResizableTableContainer# A `ResizableTableContainer` can be targeted with the `.react-aria-ResizableTableContainer` CSS selector, or by overriding with a custom `className`. ### ColumnResizer# A `ColumnResizer` can be targeted with the `.react-aria-ColumnResizer` CSS selector, or by overriding with a custom `className`. It supports the following states and render props: | Name | CSS Selector | Description | | --- | --- | --- | | `isHovered` | `[data-hovered]` | Whether the resizer is currently hovered with a mouse. | | `isFocused` | `[data-focused]` | Whether the resizer is currently focused. | | `isFocusVisible` | `[data-focus-visible]` | Whether the resizer is currently keyboard focused. | | `isResizing` | `[data-resizing]` | Whether the resizer is currently being resized. | | `resizableDirection` | `[data-resizable-direction="right | left | both"]` | The direction that the column is currently resizable. | ## Advanced customization# * * * ### Contexts# All React Aria Components export a corresponding context that can be used to send props to them from a parent element. This enables you to build your own compositional APIs similar to those found in React Aria Components itself. You can send any prop or ref via context that you could pass to the corresponding component. The local props and ref on the component are merged with the ones passed via context, with the local props taking precedence (following the rules documented in mergeProps). | Component | Context | Props | Ref | | --- | --- | --- | --- | | `Table` | `TableContext` | ` TableProps ` | `HTMLTableElement` | This example shows a component that accepts a `Table` and a ToggleButton as children, and allows the user to turn selection mode for the table on and off by pressing the button. import type {SelectionMode} from 'react-aria-components'; import {ToggleButtonContext, TableContext} from 'react-aria-components'; function Selectable({children}) { let [isSelected, onChange] = React.useState(false); let selectionMode: SelectionMode = isSelected ? 'multiple' : 'none'; return ( <ToggleButtonContext.Provider value={{isSelected, onChange}}> <TableContext.Provider value={{selectionMode}}> {children} </TableContext.Provider> </ToggleButtonContext.Provider> ); } import type {SelectionMode} from 'react-aria-components'; import { TableContext, ToggleButtonContext } from 'react-aria-components'; function Selectable({ children }) { let [isSelected, onChange] = React.useState(false); let selectionMode: SelectionMode = isSelected ? 'multiple' : 'none'; return ( <ToggleButtonContext.Provider value={{ isSelected, onChange }} > <TableContext.Provider value={{ selectionMode }}> {children} </TableContext.Provider> </ToggleButtonContext.Provider> ); } import type {SelectionMode} from 'react-aria-components'; import { TableContext, ToggleButtonContext } from 'react-aria-components'; function Selectable( { children } ) { let [ isSelected, onChange ] = React.useState( false ); let selectionMode: SelectionMode = isSelected ? 'multiple' : 'none'; return ( <ToggleButtonContext.Provider value={{ isSelected, onChange }} > <TableContext.Provider value={{ selectionMode }} > {children} </TableContext.Provider> </ToggleButtonContext.Provider> ); } The `Selectable` component can be reused to make the selection mode of any nested `Table` controlled by a `ToggleButton`. import {ToggleButton} from 'react-aria-components'; <Selectable> <ToggleButton>Select</ToggleButton> <PokemonTable /> </Selectable> import {ToggleButton} from 'react-aria-components'; <Selectable> <ToggleButton>Select</ToggleButton> <PokemonTable /> </Selectable> import {ToggleButton} from 'react-aria-components'; <Selectable> <ToggleButton> Select </ToggleButton> <PokemonTable /> </Selectable> Select | Name | Type | Level | | --- | --- | --- | | Charizard | Fire, Flying | 67 | | Blastoise | Water | 56 | | Venusaur | Grass, Poison | 83 | | Pikachu | Electric | 100 | Show CSS .react-aria-ToggleButton { margin-bottom: 8px; } .react-aria-ToggleButton { margin-bottom: 8px; } .react-aria-ToggleButton { margin-bottom: 8px; } ### Custom children# Table passes props to its child components, such as the selection checkboxes, via their associated contexts. These contexts are exported so you can also consume them in your own custom components. This enables you to reuse existing components from your app or component library together with React Aria Components. | Component | Context | Props | Ref | | --- | --- | --- | --- | | `Checkbox` | `CheckboxContext` | ` CheckboxProps ` | `HTMLLabelElement` | | `Button` | `ButtonContext` | ` ButtonProps ` | `HTMLButtonElement` | This example consumes from `CheckboxContext` in an existing styled checkbox component to make it compatible with React Aria Components. The `useContextProps` hook merges the local props and ref with the ones provided via context by Table. See useCheckbox for more details about the hooks used in this example. import type {CheckboxProps} from 'react-aria-components'; import {CheckboxContext, useContextProps} from 'react-aria-components'; import {useToggleState} from 'react-stately'; import {useCheckbox} from 'react-aria'; const MyCustomCheckbox = React.forwardRef( (props: CheckboxProps, ref: React.ForwardedRef<HTMLInputElement>) => { // Merge the local props and ref with the ones provided via context. [props, ref] = useContextProps(props, ref, CheckboxContext); let state = useToggleState(props); let { inputProps } = useCheckbox(props, state, ref); return <input {...inputProps} ref={ref} />; } ); import type {CheckboxProps} from 'react-aria-components'; import { CheckboxContext, useContextProps } from 'react-aria-components'; import {useToggleState} from 'react-stately'; import {useCheckbox} from 'react-aria'; const MyCustomCheckbox = React.forwardRef( ( props: CheckboxProps, ref: React.ForwardedRef<HTMLInputElement> ) => { // Merge the local props and ref with the ones provided via context. [props, ref] = useContextProps( props, ref, CheckboxContext ); let state = useToggleState(props); let { inputProps } = useCheckbox(props, state, ref); return <input {...inputProps} ref={ref} />; } ); import type {CheckboxProps} from 'react-aria-components'; import { CheckboxContext, useContextProps } from 'react-aria-components'; import {useToggleState} from 'react-stately'; import {useCheckbox} from 'react-aria'; const MyCustomCheckbox = React.forwardRef( ( props: CheckboxProps, ref: React.ForwardedRef< HTMLInputElement > ) => { // Merge the local props and ref with the ones provided via context. [props, ref] = useContextProps( props, ref, CheckboxContext ); let state = useToggleState( props ); let { inputProps } = useCheckbox( props, state, ref ); return ( <input {...inputProps} ref={ref} /> ); } ); Now you can use `MyCustomCheckbox` within a `Table`, in place of the builtin React Aria Components `Checkbox`. <Table> <TableHeader> {/* ... */} </TableHeader> <TableBody> <Row> <Cell><MyCustomCheckbox slot="selection" /></Cell> {/* ... */} </Row> </TableBody> </Table> <Table> <TableHeader> {/* ... */} </TableHeader> <TableBody> <Row> <Cell><MyCustomCheckbox slot="selection" /></Cell> {/* ... */} </Row> </TableBody> </Table> <Table> <TableHeader> {/* ... */} </TableHeader> <TableBody> <Row> <Cell> <MyCustomCheckbox slot="selection" /> </Cell> {/* ... */} </Row> </TableBody> </Table> ### Hooks# If you need to customize things even further, such as accessing internal state or customizing DOM structure, you can drop down to the lower level Hook-based API. See useTable for more details. ## Testing# * * * ### Test utils alpha# `@react-aria/test-utils` offers common table interaction utilities which you may find helpful when writing tests. See here for more information on how to setup these utilities in your tests. Below is the full definition of the table tester and a sample of how you could use it in your test suite. // Table.test.ts import {render, within} from '@testing-library/react'; import {User} from '@react-aria/test-utils'; let testUtilUser = new User({ interactionType: 'mouse', advanceTimer: jest.advanceTimersByTime }); // ... it('Table can toggle row selection', async function () { // Render your test component/app and initialize the table tester let { getByTestId } = render( <Table data-testid="test-table" selectionMode="multiple"> ... </Table> ); let tableTester = testUtilUser.createTester('Table', { root: getByTestId('test-table') }); expect(tableTester.selectedRows).toHaveLength(0); await tableTester.toggleSelectAll(); expect(tableTester.selectedRows).toHaveLength(10); await tableTester.toggleRowSelection({ row: 2 }); expect(tableTester.selectedRows).toHaveLength(9); let checkbox = within(tableTester.rows[2]).getByRole('checkbox'); expect(checkbox).not.toBeChecked(); await tableTester.toggleSelectAll(); expect(tableTester.selectedRows).toHaveLength(10); expect(checkbox).toBeChecked(); await tableTester.toggleSelectAll(); expect(tableTester.selectedRows).toHaveLength(0); }); // Table.test.ts import {render, within} from '@testing-library/react'; import {User} from '@react-aria/test-utils'; let testUtilUser = new User({ interactionType: 'mouse', advanceTimer: jest.advanceTimersByTime }); // ... it('Table can toggle row selection', async function () { // Render your test component/app and initialize the table tester let { getByTestId } = render( <Table data-testid="test-table" selectionMode="multiple" > ... </Table> ); let tableTester = testUtilUser.createTester('Table', { root: getByTestId('test-table') }); expect(tableTester.selectedRows).toHaveLength(0); await tableTester.toggleSelectAll(); expect(tableTester.selectedRows).toHaveLength(10); await tableTester.toggleRowSelection({ row: 2 }); expect(tableTester.selectedRows).toHaveLength(9); let checkbox = within(tableTester.rows[2]).getByRole( 'checkbox' ); expect(checkbox).not.toBeChecked(); await tableTester.toggleSelectAll(); expect(tableTester.selectedRows).toHaveLength(10); expect(checkbox).toBeChecked(); await tableTester.toggleSelectAll(); expect(tableTester.selectedRows).toHaveLength(0); }); // Table.test.ts import { render, within } from '@testing-library/react'; import {User} from '@react-aria/test-utils'; let testUtilUser = new User({ interactionType: 'mouse', advanceTimer: jest .advanceTimersByTime }); // ... it('Table can toggle row selection', async function () { // Render your test component/app and initialize the table tester let { getByTestId } = render( <Table data-testid="test-table" selectionMode="multiple" > ... </Table> ); let tableTester = testUtilUser .createTester( 'Table', { root: getByTestId( 'test-table' ) } ); expect( tableTester .selectedRows ).toHaveLength(0); await tableTester .toggleSelectAll(); expect( tableTester .selectedRows ).toHaveLength(10); await tableTester .toggleRowSelection({ row: 2 }); expect( tableTester .selectedRows ).toHaveLength(9); let checkbox = within( tableTester.rows[2] ).getByRole( 'checkbox' ); expect(checkbox).not .toBeChecked(); await tableTester .toggleSelectAll(); expect( tableTester .selectedRows ).toHaveLength(10); expect(checkbox) .toBeChecked(); await tableTester .toggleSelectAll(); expect( tableTester .selectedRows ).toHaveLength(0); }); ### Properties | Name | Type | Description | | --- | --- | --- | | `table` | `HTMLElement` | Returns the table. | | `rowGroups` | `HTMLElement[]` | Returns the row groups within the table. | | `columns` | `HTMLElement[]` | Returns the columns within the table. | | `rows` | `HTMLElement[]` | Returns the rows within the table if any. | | `selectedRows` | `HTMLElement[]` | Returns the currently selected rows within the table if any. | | `rowHeaders` | `HTMLElement[]` | Returns the row headers within the table if any. | ### Methods | Method | Description | | --- | --- | | `constructor( (opts: TableTesterOpts )): void` | | | `setInteractionType( (type: UserOpts ['interactionType'] )): void` | Set the interaction type used by the table tester. | | `toggleRowSelection( (opts: TableToggleRowOpts )): Promise<void>` | Toggles the selection for the specified table row. Defaults to using the interaction type set on the table tester. | | `toggleSort( (opts: TableToggleSortOpts )): Promise<void>` | Toggles the sort order for the specified table column. Defaults to using the interaction type set on the table tester. | | `triggerColumnHeaderAction( (opts: TableColumnHeaderActionOpts )): Promise<void>` | Triggers an action for the specified table column menu. Defaults to using the interaction type set on the table tester. | | `triggerRowAction( (opts: TableRowActionOpts )): Promise<void>` | Triggers the action for the specified table row. Defaults to using the interaction type set on the table tester. | | `toggleSelectAll( (opts: { interactionType?: UserOpts ['interactionType'] } )): Promise<void>` | Toggle selection for all rows in the table. Defaults to using the interaction type set on the table tester. | | `findRow( (opts: { rowIndexOrText: number | | string } )): HTMLElement` | Returns a row matching the specified index or text content. | | `findCell( (opts: { text: string } )): HTMLElement` | Returns a cell matching the specified text content. | | `cells( (opts: { element?: HTMLElement } )): HTMLElement[]` | Returns the cells within the table if any. Can be filtered against a specific row if provided via `element`. | --- ## Page: https://react-spectrum.adobe.com/react-aria/TagGroup.html # TagGroup A tag group is a focusable list of labels, categories, keywords, filters, or other items, with support for keyboard navigation, selection, and removal. <table><tbody><tr><th>install</th><td><code>yarn add react-aria-components</code></td></tr><tr><th>version</th><td>1.10.1</td></tr><tr><th>usage</th><td><code><span>import</span> {TagGroup, TagList, Tag} <span>from</span> <span>'react-aria-components'</span></code></td></tr></tbody></table> View ARIA pattern W3C View repository GitHub View package NPM ## Example# * * * import {TagGroup, TagList, Tag, Label} from 'react-aria-components'; <TagGroup selectionMode="multiple"> <Label>Categories</Label> <TagList> <Tag>News</Tag> <Tag>Travel</Tag> <Tag>Gaming</Tag> <Tag>Shopping</Tag> </TagList> </TagGroup> import { Label, Tag, TagGroup, TagList } from 'react-aria-components'; <TagGroup selectionMode="multiple"> <Label>Categories</Label> <TagList> <Tag>News</Tag> <Tag>Travel</Tag> <Tag>Gaming</Tag> <Tag>Shopping</Tag> </TagList> </TagGroup> import { Label, Tag, TagGroup, TagList } from 'react-aria-components'; <TagGroup selectionMode="multiple"> <Label> Categories </Label> <TagList> <Tag>News</Tag> <Tag>Travel</Tag> <Tag>Gaming</Tag> <Tag>Shopping</Tag> </TagList> </TagGroup> Categories News Travel Gaming Shopping Show CSS @import "@react-aria/example-theme"; .react-aria-TagGroup { display: flex; flex-direction: column; gap: 2px; font-size: small; color: var(--text-color); } .react-aria-TagList { display: flex; flex-wrap: wrap; gap: 4px; } .react-aria-Tag { color: var(--text-color); border: 1px solid var(--border-color); forced-color-adjust: none; border-radius: 4px; padding: 2px 8px; font-size: 0.929rem; outline: none; cursor: default; display: flex; align-items: center; transition: border-color 200ms; &[data-hovered] { border-color: var(--border-color-hover); } &[data-focus-visible] { outline: 2px solid var(--focus-ring-color); outline-offset: 2px; } &[data-selected] { border-color: var(--highlight-background); background: var(--highlight-background); color: var(--highlight-foreground); } } @import "@react-aria/example-theme"; .react-aria-TagGroup { display: flex; flex-direction: column; gap: 2px; font-size: small; color: var(--text-color); } .react-aria-TagList { display: flex; flex-wrap: wrap; gap: 4px; } .react-aria-Tag { color: var(--text-color); border: 1px solid var(--border-color); forced-color-adjust: none; border-radius: 4px; padding: 2px 8px; font-size: 0.929rem; outline: none; cursor: default; display: flex; align-items: center; transition: border-color 200ms; &[data-hovered] { border-color: var(--border-color-hover); } &[data-focus-visible] { outline: 2px solid var(--focus-ring-color); outline-offset: 2px; } &[data-selected] { border-color: var(--highlight-background); background: var(--highlight-background); color: var(--highlight-foreground); } } @import "@react-aria/example-theme"; .react-aria-TagGroup { display: flex; flex-direction: column; gap: 2px; font-size: small; color: var(--text-color); } .react-aria-TagList { display: flex; flex-wrap: wrap; gap: 4px; } .react-aria-Tag { color: var(--text-color); border: 1px solid var(--border-color); forced-color-adjust: none; border-radius: 4px; padding: 2px 8px; font-size: 0.929rem; outline: none; cursor: default; display: flex; align-items: center; transition: border-color 200ms; &[data-hovered] { border-color: var(--border-color-hover); } &[data-focus-visible] { outline: 2px solid var(--focus-ring-color); outline-offset: 2px; } &[data-selected] { border-color: var(--highlight-background); background: var(--highlight-background); color: var(--highlight-foreground); } } ## Features# * * * A static tag list can be built using <ul> or <ol> HTML elements, but does not support any user interactions. HTML lists are meant for static content, rather than lists with rich interactions such as keyboard navigation, item selection, removal, etc. `TagGroup` helps achieve accessible and interactive tag list components that can be styled as needed. * **Keyboard navigation** – Tags can be navigated using the arrow keys, along with page up/down, home/end, etc. * **Removable** – Tags can be removed from the tag group by clicking a remove button or pressing the backspace key. * **Item selection** – Single or multiple selection, with support for disabled items and both `toggle` and `replace` selection behaviors. * **Accessible** – Follows the ARIA grid pattern, with additional selection announcements via an ARIA live region. Extensively tested across many devices and assistive technologies to ensure announcements and behaviors are consistent. * **Styleable** – Items include builtin states for styling, such as hover, press, focus, selected, and disabled. ## Anatomy# * * * A tag group consists of label and a list of tags. Each tag should include a visual label, and may optionally include a remove button. If a visual label is not included in a tag group, then an `aria-label` or `aria-labelledby` prop must be passed to identify it to assistive technology. `TagGroup` also supports optional description and error message slots, which can be used to provide more context about the tag group, and any validation messages. These are linked with the tag group via the `aria-describedby` attribute. import {Button, Label, Tag, TagGroup, TagList, Text} from 'react-aria-components'; <TagGroup> <Label /> <TagList> <Tag> <Button slot="remove" /> </Tag> </TagList> <Text slot="description" /> <Text slot="errorMessage" /> </TagGroup> import { Button, Label, Tag, TagGroup, TagList, Text } from 'react-aria-components'; <TagGroup> <Label /> <TagList> <Tag> <Button slot="remove" /> </Tag> </TagList> <Text slot="description" /> <Text slot="errorMessage" /> </TagGroup> import { Button, Label, Tag, TagGroup, TagList, Text } from 'react-aria-components'; <TagGroup> <Label /> <TagList> <Tag> <Button slot="remove" /> </Tag> </TagList> <Text slot="description" /> <Text slot="errorMessage" /> </TagGroup> ### Concepts# `TagGroup` makes use of the following concepts: Collections Defining collections of items, async loading, and updating items over time. Selection Interactions and data structures to represent selection. ### Composed components# A `TagGroup` uses the following components, which may also be used standalone or reused in other components. Label A label provides context for an element. Button A button allows a user to perform an action. ## Starter kits# * * * To help kick-start your project, we offer starter kits that include example implementations of all React Aria components with various styling solutions. All components are fully styled, including support for dark mode, high contrast mode, and all UI states. Each starter comes with a pre-configured Storybook that you can experiment with, or use as a starting point for your own component library. Vanilla CSS Download ZIP Preview Tailwind CSS Download ZIP Preview ## Reusable wrappers# * * * If you will use a TagGroup in multiple places in your app, you can wrap all of the pieces into a reusable component. This way, the DOM structure, styling code, and other logic are defined in a single place and reused everywhere to ensure consistency. This example wraps `TagGroup` and all of its children together into a single component which accepts a `label` prop and `children`, which are passed through to the right places. It also shows how to use the `description` and `errorMessage` slots to render help text (see below for details). The `Tag` component is also wrapped to automatically render a remove button when the `onRemove` prop is provided to the TagGroup. import type {TagGroupProps, TagListProps, TagProps} from 'react-aria-components'; import {Button, Text} from 'react-aria-components'; interface MyTagGroupProps<T> extends Omit<TagGroupProps, 'children'>, Pick<TagListProps<T>, 'items' | 'children' | 'renderEmptyState'> { label?: string; description?: string; errorMessage?: string; } function MyTagGroup<T extends object>( { label, description, errorMessage, items, children, renderEmptyState, ...props }: MyTagGroupProps<T> ) { return ( <TagGroup {...props}> <Label>{label}</Label> <TagList items={items} renderEmptyState={renderEmptyState}> {children} </TagList> {description && <Text slot="description">{description}</Text>} {errorMessage && <Text slot="errorMessage">{errorMessage}</Text>} </TagGroup> ); } function MyTag( { children, ...props }: Omit<TagProps, 'children'> & { children?: React.ReactNode; } ) { let textValue = typeof children === 'string' ? children : undefined; return ( <Tag textValue={textValue} {...props}> {({ allowsRemoving }) => ( <> {children} {allowsRemoving && <Button slot="remove">ⓧ</Button>} </> )} </Tag> ); } <MyTagGroup label="Ice cream flavor" selectionMode="single"> <MyTag>Chocolate</MyTag> <MyTag>Mint</MyTag> <MyTag>Strawberry</MyTag> <MyTag>Vanilla</MyTag> </MyTagGroup> import type { TagGroupProps, TagListProps, TagProps } from 'react-aria-components'; import {Button, Text} from 'react-aria-components'; interface MyTagGroupProps<T> extends Omit<TagGroupProps, 'children'>, Pick< TagListProps<T>, 'items' | 'children' | 'renderEmptyState' > { label?: string; description?: string; errorMessage?: string; } function MyTagGroup<T extends object>( { label, description, errorMessage, items, children, renderEmptyState, ...props }: MyTagGroupProps<T> ) { return ( <TagGroup {...props}> <Label>{label}</Label> <TagList items={items} renderEmptyState={renderEmptyState} > {children} </TagList> {description && ( <Text slot="description">{description}</Text> )} {errorMessage && ( <Text slot="errorMessage">{errorMessage}</Text> )} </TagGroup> ); } function MyTag( { children, ...props }: Omit<TagProps, 'children'> & { children?: React.ReactNode; } ) { let textValue = typeof children === 'string' ? children : undefined; return ( <Tag textValue={textValue} {...props}> {({ allowsRemoving }) => ( <> {children} {allowsRemoving && ( <Button slot="remove">ⓧ</Button> )} </> )} </Tag> ); } <MyTagGroup label="Ice cream flavor" selectionMode="single" > <MyTag>Chocolate</MyTag> <MyTag>Mint</MyTag> <MyTag>Strawberry</MyTag> <MyTag>Vanilla</MyTag> </MyTagGroup> import type { TagGroupProps, TagListProps, TagProps } from 'react-aria-components'; import { Button, Text } from 'react-aria-components'; interface MyTagGroupProps< T > extends Omit< TagGroupProps, 'children' >, Pick< TagListProps<T>, | 'items' | 'children' | 'renderEmptyState' > { label?: string; description?: string; errorMessage?: string; } function MyTagGroup< T extends object >({ label, description, errorMessage, items, children, renderEmptyState, ...props }: MyTagGroupProps<T>) { return ( <TagGroup {...props}> <Label> {label} </Label> <TagList items={items} renderEmptyState={renderEmptyState} > {children} </TagList> {description && ( <Text slot="description"> {description} </Text> )} {errorMessage && ( <Text slot="errorMessage"> {errorMessage} </Text> )} </TagGroup> ); } function MyTag({ children, ...props }: & Omit< TagProps, 'children' > & { children?: React.ReactNode; }) { let textValue = typeof children === 'string' ? children : undefined; return ( <Tag textValue={textValue} {...props} > {( { allowsRemoving } ) => ( <> {children} {allowsRemoving && ( <Button slot="remove"> ⓧ </Button> )} </> )} </Tag> ); } <MyTagGroup label="Ice cream flavor" selectionMode="single" > <MyTag> Chocolate </MyTag> <MyTag>Mint</MyTag> <MyTag> Strawberry </MyTag> <MyTag> Vanilla </MyTag> </MyTagGroup> Ice cream flavor Chocolate Mint Strawberry Vanilla ## Removing tags# * * * The `onRemove` prop can be used to allow the user to remove tags. In the above example, an additional `<Button slot="remove>` element is rendered when a tag group allows removing. The user can also press the backspace key while a tag is focused to remove the tag from the group. Additionally, when selection is enabled, all selected items will be deleted when pressing the backspace key on a selected tag. import {useListData} from 'react-stately'; function Example() { let list = useListData({ initialItems: [ { id: 1, name: 'News' }, { id: 2, name: 'Travel' }, { id: 3, name: 'Gaming' }, { id: 4, name: 'Shopping' } ] }); return ( <MyTagGroup label="Categories" items={list.items} onRemove={(keys) => list.remove(...keys)} > {(item) => <MyTag>{item.name}</MyTag>} </MyTagGroup> ); } import {useListData} from 'react-stately'; function Example() { let list = useListData({ initialItems: [ { id: 1, name: 'News' }, { id: 2, name: 'Travel' }, { id: 3, name: 'Gaming' }, { id: 4, name: 'Shopping' } ] }); return ( <MyTagGroup label="Categories" items={list.items} onRemove={(keys) => list.remove(...keys)} > {(item) => <MyTag>{item.name}</MyTag>} </MyTagGroup> ); } import {useListData} from 'react-stately'; function Example() { let list = useListData( { initialItems: [ { id: 1, name: 'News' }, { id: 2, name: 'Travel' }, { id: 3, name: 'Gaming' }, { id: 4, name: 'Shopping' } ] } ); return ( <MyTagGroup label="Categories" items={list.items} onRemove={(keys) => list.remove( ...keys )} > {(item) => ( <MyTag> {item.name} </MyTag> )} </MyTagGroup> ); } Categories Newsⓧ Travelⓧ Gamingⓧ Shoppingⓧ Show CSS .react-aria-Tag { [slot=remove] { background: none; border: none; padding: 0; margin-left: 2px; color: var(--text-color-base); transition: color 200ms; outline: none; font-size: 0.95em; border-radius: 100%; aspect-ratio: 1/1; height: 100%; &[data-hovered] { color: var(--text-color-hover); } &[data-focus-visible] { outline: 2px solid var(--focus-ring-color); outline-offset: -1px; } } &[data-selected] { [slot=remove] { color: inherit; } } } .react-aria-Tag { [slot=remove] { background: none; border: none; padding: 0; margin-left: 2px; color: var(--text-color-base); transition: color 200ms; outline: none; font-size: 0.95em; border-radius: 100%; aspect-ratio: 1/1; height: 100%; &[data-hovered] { color: var(--text-color-hover); } &[data-focus-visible] { outline: 2px solid var(--focus-ring-color); outline-offset: -1px; } } &[data-selected] { [slot=remove] { color: inherit; } } } .react-aria-Tag { [slot=remove] { background: none; border: none; padding: 0; margin-left: 2px; color: var(--text-color-base); transition: color 200ms; outline: none; font-size: 0.95em; border-radius: 100%; aspect-ratio: 1/1; height: 100%; &[data-hovered] { color: var(--text-color-hover); } &[data-focus-visible] { outline: 2px solid var(--focus-ring-color); outline-offset: -1px; } } &[data-selected] { [slot=remove] { color: inherit; } } } ## Selection# * * * TagGroup supports multiple selection modes. By default, selection is disabled, however this can be changed using the `selectionMode` prop. Use `defaultSelectedKeys` to provide a default set of selected items (uncontrolled) and `selectedKeys` to set the selected items (controlled). The value of the selected keys must match the `id` prop of the items. See the Selection guide for more details. import type {Selection} from 'react-aria-components'; function Example() { let [selected, setSelected] = React.useState<Selection>(new Set(['parking'])); return ( <> <MyTagGroup label="Amenities" selectionMode="multiple" selectedKeys={selected} onSelectionChange={setSelected} > <MyTag id="laundry">Laundry</MyTag> <MyTag id="fitness">Fitness center</MyTag> <MyTag id="parking">Parking</MyTag> <MyTag id="pool">Swimming pool</MyTag> <MyTag id="breakfast">Breakfast</MyTag> </MyTagGroup> <p> Current selection (controlled):{' '} {selected === 'all' ? 'all' : [...selected].join(', ')} </p> </> ); } import type {Selection} from 'react-aria-components'; function Example() { let [selected, setSelected] = React.useState<Selection>( new Set(['parking']) ); return ( <> <MyTagGroup label="Amenities" selectionMode="multiple" selectedKeys={selected} onSelectionChange={setSelected} > <MyTag id="laundry">Laundry</MyTag> <MyTag id="fitness">Fitness center</MyTag> <MyTag id="parking">Parking</MyTag> <MyTag id="pool">Swimming pool</MyTag> <MyTag id="breakfast">Breakfast</MyTag> </MyTagGroup> <p> Current selection (controlled): {selected === 'all' ? 'all' : [...selected].join(', ')} </p> </> ); } import type {Selection} from 'react-aria-components'; function Example() { let [ selected, setSelected ] = React.useState< Selection >( new Set(['parking']) ); return ( <> <MyTagGroup label="Amenities" selectionMode="multiple" selectedKeys={selected} onSelectionChange={setSelected} > <MyTag id="laundry"> Laundry </MyTag> <MyTag id="fitness"> Fitness center </MyTag> <MyTag id="parking"> Parking </MyTag> <MyTag id="pool"> Swimming pool </MyTag> <MyTag id="breakfast"> Breakfast </MyTag> </MyTagGroup> <p> Current selection (controlled): {' '} {selected === 'all' ? 'all' : [...selected] .join(', ')} </p> </> ); } Amenities Laundry Fitness center Parking Swimming pool Breakfast Current selection (controlled): parking ## Links# * * * Tags may be links to another page or website. This can be achieved by passing the `href` prop to the `<Tag>` component. Tags with an `href` are not selectable. <MyTagGroup label="Links"> <MyTag href="https://adobe.com/" target="_blank">Adobe</MyTag> <MyTag href="https://apple.com/" target="_blank">Apple</MyTag> <MyTag href="https://google.com/" target="_blank">Google</MyTag> <MyTag href="https://microsoft.com/" target="_blank">Microsoft</MyTag> </MyTagGroup> <MyTagGroup label="Links"> <MyTag href="https://adobe.com/" target="_blank"> Adobe </MyTag> <MyTag href="https://apple.com/" target="_blank"> Apple </MyTag> <MyTag href="https://google.com/" target="_blank"> Google </MyTag> <MyTag href="https://microsoft.com/" target="_blank"> Microsoft </MyTag> </MyTagGroup> <MyTagGroup label="Links"> <MyTag href="https://adobe.com/" target="_blank" > Adobe </MyTag> <MyTag href="https://apple.com/" target="_blank" > Apple </MyTag> <MyTag href="https://google.com/" target="_blank" > Google </MyTag> <MyTag href="https://microsoft.com/" target="_blank" > Microsoft </MyTag> </MyTagGroup> Links Adobe Apple Google Microsoft ### Client side routing# The `<Tag>` component works with frameworks and client side routers like Next.js and React Router. As with other React Aria components that support links, this works via the `RouterProvider` component at the root of your app. See the client side routing guide to learn how to set this up. ## Disabled tags# * * * A `Tag` can be disabled with the `isDisabled` prop. Disabled tags are not focusable, selectable, or keyboard navigable. <MyTagGroup label="Sandwich contents" selectionMode="multiple" > <MyTag>Lettuce</MyTag> <MyTag>Tomato</MyTag> <MyTag>Cheese</MyTag> <MyTag isDisabled>Tuna Salad</MyTag> <MyTag>Egg Salad</MyTag> <MyTag>Ham</MyTag> </MyTagGroup> <MyTagGroup label="Sandwich contents" selectionMode="multiple" > <MyTag>Lettuce</MyTag> <MyTag>Tomato</MyTag> <MyTag>Cheese</MyTag> <MyTag isDisabled>Tuna Salad</MyTag> <MyTag>Egg Salad</MyTag> <MyTag>Ham</MyTag> </MyTagGroup> <MyTagGroup label="Sandwich contents" selectionMode="multiple" > <MyTag> Lettuce </MyTag> <MyTag>Tomato</MyTag> <MyTag>Cheese</MyTag> <MyTag isDisabled> Tuna Salad </MyTag> <MyTag> Egg Salad </MyTag> <MyTag>Ham</MyTag> </MyTagGroup> Sandwich contents Lettuce Tomato Cheese Tuna Salad Egg Salad Ham Show CSS .react-aria-TagList { .react-aria-Tag { &[data-disabled] { border-color: var(--border-color-disabled); color: var(--text-color-disabled); } } } .react-aria-TagList { .react-aria-Tag { &[data-disabled] { border-color: var(--border-color-disabled); color: var(--text-color-disabled); } } } .react-aria-TagList { .react-aria-Tag { &[data-disabled] { border-color: var(--border-color-disabled); color: var(--text-color-disabled); } } } In dynamic collections, it may be more convenient to use the `disabledKeys` prop at the `TagGroup` level instead of `isDisabled` on individual tags. Each key in this list corresponds with the `id` prop passed to the `Tag` component, or automatically derived from the values passed to the `items` prop (see the Collections for more details). A tag is considered disabled if its id exists in `disabledKeys` or if it has `isDisabled`. function Example() { let options = [ { id: 1, name: "News" }, { id: 2, name: "Travel" }, { id: 3, name: "Gaming" }, { id: 4, name: "Shopping" } ]; return ( <MyTagGroup label="Categories" items={options} selectionMode="multiple" disabledKeys={[2, 4]} > {(item) => <MyTag>{item.name}</MyTag>} </MyTagGroup> ); } function Example() { let options = [ { id: 1, name: "News" }, { id: 2, name: "Travel" }, { id: 3, name: "Gaming" }, { id: 4, name: "Shopping" } ]; return ( <MyTagGroup label="Categories" items={options} selectionMode="multiple" disabledKeys={[2, 4]} > {(item) => <MyTag>{item.name}</MyTag>} </MyTagGroup> ); } function Example() { let options = [ { id: 1, name: 'News' }, { id: 2, name: 'Travel' }, { id: 3, name: 'Gaming' }, { id: 4, name: 'Shopping' } ]; return ( <MyTagGroup label="Categories" items={options} selectionMode="multiple" disabledKeys={[ 2, 4 ]} > {(item) => ( <MyTag> {item.name} </MyTag> )} </MyTagGroup> ); } Categories News Travel Gaming Shopping ## Empty state# * * * Use the `renderEmptyState` prop to customize what a `TagList` will display if there are no items. <TagGroup> <Label>Categories</Label> <TagList renderEmptyState={() => 'No categories.'}> {[]} </TagList> </TagGroup> <TagGroup> <Label>Categories</Label> <TagList renderEmptyState={() => 'No categories.'}> {[]} </TagList> </TagGroup> <TagGroup> <Label> Categories </Label> <TagList renderEmptyState={() => 'No categories.'} > {[]} </TagList> </TagGroup> Categories No categories. ## Help text# * * * ### Description# The `description` slot can be used to associate additional help text with a `TagGroup`. <TagGroup> <Label>Categories</Label> <TagList> <Tag>News</Tag> <Tag>Travel</Tag> <Tag>Gaming</Tag> <Tag>Shopping</Tag> </TagList> <Text slot="description">Your selected categories.</Text></TagGroup> <TagGroup> <Label>Categories</Label> <TagList> <Tag>News</Tag> <Tag>Travel</Tag> <Tag>Gaming</Tag> <Tag>Shopping</Tag> </TagList> <Text slot="description">Your selected categories.</Text></TagGroup> <TagGroup> <Label> Categories </Label> <TagList> <Tag>News</Tag> <Tag>Travel</Tag> <Tag>Gaming</Tag> <Tag>Shopping</Tag> </TagList> <Text slot="description"> Your selected categories. </Text></TagGroup> Categories News Travel Gaming Shopping Your selected categories. Show CSS .react-aria-TagGroup { [slot=description] { font-size: 12px; } [slot=errorMessage] { font-size: 12px; color: var(--invalid-color); } } .react-aria-TagGroup { [slot=description] { font-size: 12px; } [slot=errorMessage] { font-size: 12px; color: var(--invalid-color); } } .react-aria-TagGroup { [slot=description] { font-size: 12px; } [slot=errorMessage] { font-size: 12px; color: var(--invalid-color); } } ### Error message# The `errorMessage` slot can be used to help the user fix a validation error. <TagGroup> <Label>Categories</Label> <TagList> <Tag>News</Tag> <Tag>Travel</Tag> <Tag>Gaming</Tag> <Tag>Shopping</Tag> </TagList> <Text slot="errorMessage">Invalid set of categories.</Text></TagGroup> <TagGroup> <Label>Categories</Label> <TagList> <Tag>News</Tag> <Tag>Travel</Tag> <Tag>Gaming</Tag> <Tag>Shopping</Tag> </TagList> <Text slot="errorMessage"> Invalid set of categories. </Text></TagGroup> <TagGroup> <Label> Categories </Label> <TagList> <Tag>News</Tag> <Tag>Travel</Tag> <Tag>Gaming</Tag> <Tag>Shopping</Tag> </TagList> <Text slot="errorMessage"> Invalid set of categories. </Text></TagGroup> Categories News Travel Gaming Shopping Invalid set of categories. ## Props# * * * ### TagGroup# | Name | Type | Description | | --- | --- | --- | | `selectionBehavior` | ` SelectionBehavior ` | How multiple selection should behave in the collection. | | `shouldSelectOnPressUp` | `boolean` | Whether selection should occur on press up instead of press down. | | `disabledKeys` | `Iterable<Key>` | The item keys that are disabled. These items cannot be selected, focused, or otherwise interacted with. | | `selectionMode` | ` SelectionMode ` | The type of selection that is allowed in the collection. | | `disallowEmptySelection` | `boolean` | Whether the collection allows empty selection. | | `selectedKeys` | `'all' | Iterable<Key>` | The currently selected keys in the collection (controlled). | | `defaultSelectedKeys` | `'all' | Iterable<Key>` | The initial selected keys in the collection (uncontrolled). | | `children` | `ReactNode` | The children of the component. | | `className` | `string` | The CSS className for the element. | | `style` | `CSSProperties` | The inline style for the element. | Events | Name | Type | Description | | --- | --- | --- | | `onRemove` | `( (keys: Set<Key> )) => void` | Handler that is called when a user deletes a tag. | | `onSelectionChange` | `( (keys: Selection )) => void` | Handler that is called when the selection changes. | Layout | Name | Type | Description | | --- | --- | --- | | `slot` | `string | null` | A slot name for the component. Slots allow the component to receive props from a parent component. An explicit `null` value indicates that the local props completely override all props received from a parent. | Accessibility | Name | Type | Description | | --- | --- | --- | | `id` | `string` | The element's unique identifier. See MDN. | | `aria-label` | `string` | Defines a string value that labels the current element. | | `aria-labelledby` | `string` | Identifies the element (or elements) that labels the current element. | | `aria-describedby` | `string` | Identifies the element (or elements) that describes the object. | | `aria-details` | `string` | Identifies the element (or elements) that provide a detailed, extended description for the object. | ### TagList# A `<TagList>` is a list of tags within a `<TagGroup>`. | Name | Type | Description | | --- | --- | --- | | `renderEmptyState` | `( (props: TagListRenderProps )) => ReactNode` | Provides content to display when there are no items in the tag list. | | `children` | `ReactNode | ( (item: T )) => ReactNode` | The contents of the collection. | | `dependencies` | `ReadonlyArray<any>` | Values that should invalidate the item cache when using dynamic collections. | | `items` | `Iterable<T>` | Item objects in the collection. | | `className` | `string | ( (values: TagListRenderProps & & { defaultClassName: string | | undefined } )) => string` | The CSS className for the element. A function may be provided to compute the class based on component state. | | `style` | `CSSProperties | ( (values: TagListRenderProps & & { defaultStyle: CSSProperties } )) => CSSProperties | undefined` | The inline style for the element. A function may be provided to compute the style based on component state. | ### Tag# A `<Tag>` defines a single item within a `<TagList>`. If the children are not plain text, then the `textValue` prop must also be set to a plain text representation for accessibility. | Name | Type | Description | | --- | --- | --- | | `id` | `Key` | A unique id for the tag. | | `textValue` | `string` | A string representation of the tags's contents, used for accessibility. Required if children is not a plain text string. | | `isDisabled` | `boolean` | Whether the tag is disabled. | | `children` | `ReactNode | ( (values: TagRenderProps & & { defaultChildren: ReactNode | | undefined } )) => ReactNode` | The children of the component. A function may be provided to alter the children based on component state. | | `className` | `string | ( (values: TagRenderProps & & { defaultClassName: string | | undefined } )) => string` | The CSS className for the element. A function may be provided to compute the class based on component state. | | `style` | `CSSProperties | ( (values: TagRenderProps & & { defaultStyle: CSSProperties } )) => CSSProperties | undefined` | The inline style for the element. A function may be provided to compute the style based on component state. | | `href` | ` Href ` | A URL to link to. See MDN. | | `hrefLang` | `string` | Hints at the human language of the linked URL. SeeMDN. | | `target` | `HTMLAttributeAnchorTarget` | The target window for the link. See MDN. | | `rel` | `string` | The relationship between the linked resource and the current page. See MDN. | | `download` | `boolean | string` | Causes the browser to download the linked URL. A string may be provided to suggest a file name. See MDN. | | `ping` | `string` | A space-separated list of URLs to ping when the link is followed. See MDN. | | `referrerPolicy` | `HTMLAttributeReferrerPolicy` | How much of the referrer to send when following the link. See MDN. | | `routerOptions` | ` RouterOptions ` | Options for the configured client side router. | Events | Name | Type | Description | | --- | --- | --- | | `onHoverStart` | `( (e: HoverEvent )) => void` | Handler that is called when a hover interaction starts. | | `onHoverEnd` | `( (e: HoverEvent )) => void` | Handler that is called when a hover interaction ends. | | `onHoverChange` | `( (isHovering: boolean )) => void` | Handler that is called when the hover state changes. | ### Label# A `<Label>` accepts all HTML attributes. ### Text# `<Text>` accepts all HTML attributes. ## Styling# * * * React Aria components can be styled in many ways, including using CSS classes, inline styles, utility classes (e.g. Tailwind), CSS-in-JS (e.g. Styled Components), etc. By default, all components include a builtin `className` attribute which can be targeted using CSS selectors. These follow the `react-aria-ComponentName` naming convention. .react-aria-TagGroup { /* ... */ } .react-aria-TagGroup { /* ... */ } .react-aria-TagGroup { /* ... */ } A custom `className` can also be specified on any component. This overrides the default `className` provided by React Aria with your own. <TagGroup className="my-tag-group"> {/* ... */} </TagGroup> <TagGroup className="my-tag-group"> {/* ... */} </TagGroup> <TagGroup className="my-tag-group"> {/* ... */} </TagGroup> In addition, some components support multiple UI states (e.g. pressed, hovered, etc.). React Aria components expose states using data attributes, which you can target in CSS selectors. For example: .react-aria-Tag[data-selected] { /* ... */ } .react-aria-Tag[data-focused] { /* ... */ } .react-aria-Tag[data-selected] { /* ... */ } .react-aria-Tag[data-focused] { /* ... */ } .react-aria-Tag[data-selected] { /* ... */ } .react-aria-Tag[data-focused] { /* ... */ } The `className` and `style` props also accept functions which receive states for styling. This lets you dynamically determine the classes or styles to apply, which is useful when using utility CSS libraries like Tailwind. <Tag className={({ isSelected }) => isSelected ? 'bg-blue-400' : 'bg-gray-100'} > Item </Tag> <Tag className={({ isSelected }) => isSelected ? 'bg-blue-400' : 'bg-gray-100'} > Item </Tag> <Tag className={( { isSelected } ) => isSelected ? 'bg-blue-400' : 'bg-gray-100'} > Item </Tag> 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 remove button only when removal is allowed. <Tag> {({allowsRemoving}) => ( <> Item {allowsRemoving && <RemoveButton />} </> )} </Tag> <Tag> {({allowsRemoving}) => ( <> Item {allowsRemoving && <RemoveButton />} </> )} </Tag> <Tag> {( { allowsRemoving } ) => ( <> Item {allowsRemoving && ( <RemoveButton /> )} </> )} </Tag> The states and selectors for each component used in a `TagGroup` are documented below. ### TagGroup# A `TagGroup` can be targeted with the `.react-aria-TagGroup` CSS selector, or by overriding with a custom `className`. ### TagList# A `TagList` can be targeted with the `.react-aria-TagList` CSS selector, or by overriding with a custom `className`. It supports the following states and render props: | Name | CSS Selector | Description | | --- | --- | --- | | `isEmpty` | `[data-empty]` | Whether the tag list has no items and should display its empty state. | | `isFocused` | `[data-focused]` | Whether the tag list is currently focused. | | `isFocusVisible` | `[data-focus-visible]` | Whether the tag list is currently keyboard focused. | | `state` | `—` | State of the TagGroup. | ### Tag# A `Tag` can be targeted with the `.react-aria-Tag` CSS selector, or by overriding with a custom `className`. It supports the following states and render props: | Name | CSS Selector | Description | | --- | --- | --- | | `allowsRemoving` | `[data-allows-removing]` | Whether the tag group allows items to be removed. | | `isHovered` | `[data-hovered]` | Whether the item is currently hovered with a mouse. | | `isPressed` | `[data-pressed]` | Whether the item is currently in a pressed state. | | `isSelected` | `[data-selected]` | Whether the item is currently selected. | | `isFocused` | `[data-focused]` | Whether the item is currently focused. | | `isFocusVisible` | `[data-focus-visible]` | Whether the item is currently keyboard focused. | | `isDisabled` | `[data-disabled]` | Whether the item is non-interactive, i.e. both selection and actions are disabled and the item may not be focused. Dependent on `disabledKeys` and `disabledBehavior`. | | `selectionMode` | `[data-selection-mode="single | multiple"]` | The type of selection that is allowed in the collection. | | `selectionBehavior` | `—` | The selection behavior for the collection. | Tags also accept a `<Button slot="remove">` element as a child, which allows them to be removed. This can be conditionally rendered using the `allowsRemoving` render prop, as shown below. ### Label# A `Label` can be targeted with the `.react-aria-Label` CSS selector, or by overriding with a custom `className`. ### Text# The help text elements within a `TagGroup` can be targeted with the `[slot=description]` and `[slot=errorMessage]` CSS selectors, or by adding a custom `className`. ## Advanced customization# * * * ### Composition# If you need to customize one of the components within a `TagGroup`, such as `TagList` or `Tag`, in many cases you can create a wrapper component. This lets you customize the props passed to the component. function MyTag(props) { return <Tag {...props} className="my-tag" /> } function MyTag(props) { return <Tag {...props} className="my-tag" /> } function MyTag(props) { return ( <Tag {...props} className="my-tag" /> ); } ### Contexts# All React Aria Components export a corresponding context that can be used to send props to them from a parent element. This enables you to build your own compositional APIs similar to those found in React Aria Components itself. You can send any prop or ref via context that you could pass to the corresponding component. The local props and ref on the component are merged with the ones passed via context, with the local props taking precedence (following the rules documented in mergeProps). | Component | Context | Props | Ref | | --- | --- | --- | --- | | `TagGroup` | `TagGroupContext` | ` TagGroupProps ` | `HTMLDivElement` | This example shows a component that accepts a `TagGroup` and a ToggleButton as children, and allows the user to turn edit mode for the tag group on and off by pressing the button. import {ToggleButtonContext, TagGroupContext} from 'react-aria-components'; function Removable({children, onRemove}) { let [isSelected, onChange] = React.useState(false); return ( <ToggleButtonContext.Provider value={{isSelected, onChange}}> <TagGroupContext.Provider value={{onRemove: isSelected && onRemove}}> {children} </TagGroupContext.Provider> </ToggleButtonContext.Provider> ); } import { TagGroupContext, ToggleButtonContext } from 'react-aria-components'; function Removable({ children, onRemove }) { let [isSelected, onChange] = React.useState(false); return ( <ToggleButtonContext.Provider value={{ isSelected, onChange }} > <TagGroupContext.Provider value={{ onRemove: isSelected && onRemove }} > {children} </TagGroupContext.Provider> </ToggleButtonContext.Provider> ); } import { TagGroupContext, ToggleButtonContext } from 'react-aria-components'; function Removable( { children, onRemove } ) { let [ isSelected, onChange ] = React.useState( false ); return ( <ToggleButtonContext.Provider value={{ isSelected, onChange }} > <TagGroupContext.Provider value={{ onRemove: isSelected && onRemove }} > {children} </TagGroupContext.Provider> </ToggleButtonContext.Provider> ); } The `Removable` component can be reused to make the edit mode of any nested `TagGroup` controlled by a `ToggleButton`. import {ToggleButton} from 'react-aria-components'; <Removable onRemove={ids => alert(`Remove ${[...ids]}`)}> <MyTagGroup label="Ice cream flavor"> <MyTag id="chocolate">Chocolate</MyTag> <MyTag id="mint">Mint</MyTag> <MyTag id="strawberry">Strawberry</MyTag> <MyTag id="vanilla">Vanilla</MyTag> </MyTagGroup> <ToggleButton style={{marginTop: '8px'}}>Edit</ToggleButton> </Removable> import {ToggleButton} from 'react-aria-components'; <Removable onRemove={(ids) => alert(`Remove ${[...ids]}`)} > <MyTagGroup label="Ice cream flavor"> <MyTag id="chocolate">Chocolate</MyTag> <MyTag id="mint">Mint</MyTag> <MyTag id="strawberry">Strawberry</MyTag> <MyTag id="vanilla">Vanilla</MyTag> </MyTagGroup> <ToggleButton style={{ marginTop: '8px' }}> Edit </ToggleButton> </Removable> import {ToggleButton} from 'react-aria-components'; <Removable onRemove={(ids) => alert( `Remove ${[ ...ids ]}` )} > <MyTagGroup label="Ice cream flavor"> <MyTag id="chocolate"> Chocolate </MyTag> <MyTag id="mint"> Mint </MyTag> <MyTag id="strawberry"> Strawberry </MyTag> <MyTag id="vanilla"> Vanilla </MyTag> </MyTagGroup> <ToggleButton style={{ marginTop: '8px' }} > Edit </ToggleButton> </Removable> Ice cream flavor Chocolate Mint Strawberry Vanilla Edit ### Custom children# TagGroup passes props to its child components, such as the label, via their associated contexts. These contexts are exported so you can also consume them in your own custom components. This enables you to reuse existing components from your app or component library together with React Aria Components. | Component | Context | Props | Ref | | --- | --- | --- | --- | | `Label` | `LabelContext` | ` LabelProps ` | `HTMLLabelElement` | | `Button` | `ButtonContext` | ` ButtonProps ` | `HTMLButtonElement` | | `Text` | `TextContext` | ` TextProps ` | `HTMLElement` | This example consumes from `LabelContext` in an existing styled label component to make it compatible with React Aria Components. The `useContextProps` hook merges the local props and ref with the ones provided via context by TagGroup. import type {LabelProps} from 'react-aria-components'; import {LabelContext, useContextProps} from 'react-aria-components'; const MyCustomLabel = React.forwardRef( (props: LabelProps, ref: React.ForwardedRef<HTMLLabelElement>) => { // Merge the local props and ref with the ones provided via context. [props, ref] = useContextProps(props, ref, LabelContext); // ... your existing Label component return <label {...props} ref={ref} />; } ); import type {LabelProps} from 'react-aria-components'; import { LabelContext, useContextProps } from 'react-aria-components'; const MyCustomLabel = React.forwardRef( ( props: LabelProps, ref: React.ForwardedRef<HTMLLabelElement> ) => { // Merge the local props and ref with the ones provided via context. [props, ref] = useContextProps( props, ref, LabelContext ); // ... your existing Label component return <label {...props} ref={ref} />; } ); import type {LabelProps} from 'react-aria-components'; import { LabelContext, useContextProps } from 'react-aria-components'; const MyCustomLabel = React.forwardRef( ( props: LabelProps, ref: React.ForwardedRef< HTMLLabelElement > ) => { // Merge the local props and ref with the ones provided via context. [props, ref] = useContextProps( props, ref, LabelContext ); // ... your existing Label component return ( <label {...props} ref={ref} /> ); } ); Now you can use `MyCustomLabel` within a `TagGroup`, in place of the builtin React Aria Components `Label`. <TagGroup> <MyCustomLabel>Name</MyCustomLabel> {/* ... */} </TagGroup> <TagGroup> <MyCustomLabel>Name</MyCustomLabel> {/* ... */} </TagGroup> <TagGroup> <MyCustomLabel> Name </MyCustomLabel> {/* ... */} </TagGroup> ### State# TagGroup provides a `ListState` object to its children via `ListStateContext`. This can be used to access and manipulate the TagGroup's state. This example shows a `SelectionCount` component that can be placed within a `TagGroup` to display the number of selected tags. import {ListStateContext} from 'react-aria-components'; function SelectionCount() { let state = React.useContext(ListStateContext); let selected = state?.selectionManager.selectedKeys.size ?? 0; return <small>{selected} tags selected.</small>; } <TagGroup selectionMode="multiple"> <Label>Tags</Label> <TagList> <Tag>News</Tag> <Tag>Travel</Tag> <Tag>Gaming</Tag> <Tag>Shopping</Tag> </TagList> <SelectionCount /> </TagGroup> import {ListStateContext} from 'react-aria-components'; function SelectionCount() { let state = React.useContext(ListStateContext); let selected = state?.selectionManager.selectedKeys.size ?? 0; return <small>{selected} tags selected.</small>; } <TagGroup selectionMode="multiple"> <Label>Tags</Label> <TagList> <Tag>News</Tag> <Tag>Travel</Tag> <Tag>Gaming</Tag> <Tag>Shopping</Tag> </TagList> <SelectionCount /> </TagGroup> import {ListStateContext} from 'react-aria-components'; function SelectionCount() { let state = React .useContext( ListStateContext ); let selected = state ?.selectionManager .selectedKeys .size ?? 0; return ( <small> {selected}{' '} tags selected. </small> ); } <TagGroup selectionMode="multiple"> <Label>Tags</Label> <TagList> <Tag>News</Tag> <Tag>Travel</Tag> <Tag>Gaming</Tag> <Tag>Shopping</Tag> </TagList> <SelectionCount /> </TagGroup> Tags News Travel Gaming Shopping 0 tags selected. ### Hooks# If you need to customize things even further, such as accessing internal state, intercepting events, or customizing DOM structure, you can drop down to the lower level Hook-based API. See useTagGroup for more details. --- ## Page: https://react-spectrum.adobe.com/react-aria/Tree.html # Tree A tree provides users with a way to navigate nested hierarchical information, with support for keyboard navigation and selection. <table><tbody><tr><th>install</th><td><code>yarn add react-aria-components</code></td></tr><tr><th>version</th><td>1.10.1</td></tr><tr><th>usage</th><td><code><span>import</span> {Tree} <span>from</span> <span>'react-aria-components'</span></code></td></tr></tbody></table> View ARIA pattern W3C View repository GitHub View package NPM ## Example# * * * This example's MyTreeItemContent is from the Reusable Wrappers section below. import {Button, Collection, Tree, TreeItem, TreeItemContent} from 'react-aria-components'; <Tree aria-label="Files" style={{ height: '300px' }} defaultExpandedKeys={['documents', 'photos', 'project']} selectionMode="multiple" defaultSelectedKeys={['photos']} > <TreeItem id="documents" textValue="Documents"> <MyTreeItemContent> Documents <Button aria-label="Info">ⓘ</Button> </MyTreeItemContent> <TreeItem id="project" textValue="Project"> <MyTreeItemContent> Project <Button aria-label="Info">ⓘ</Button> </MyTreeItemContent> <TreeItem id="report" textValue="Weekly Report"> <MyTreeItemContent> Weekly Report <Button aria-label="Info">ⓘ</Button> </MyTreeItemContent> </TreeItem> </TreeItem> </TreeItem> <TreeItem id="photos" textValue="Photos"> <MyTreeItemContent> Photos <Button aria-label="Info">ⓘ</Button> </MyTreeItemContent> <TreeItem id="one" textValue="Image 1"> <MyTreeItemContent> Image 1 <Button aria-label="Info">ⓘ</Button> </MyTreeItemContent> </TreeItem> <TreeItem id="two" textValue="Image 2"> <MyTreeItemContent> Image 2 <Button aria-label="Info">ⓘ</Button> </MyTreeItemContent> </TreeItem> </TreeItem> </Tree> import { Button, Collection, Tree, TreeItem, TreeItemContent } from 'react-aria-components'; <Tree aria-label="Files" style={{ height: '300px' }} defaultExpandedKeys={['documents', 'photos', 'project']} selectionMode="multiple" defaultSelectedKeys={['photos']} > <TreeItem id="documents" textValue="Documents"> <MyTreeItemContent> Documents <Button aria-label="Info">ⓘ</Button> </MyTreeItemContent> <TreeItem id="project" textValue="Project"> <MyTreeItemContent> Project <Button aria-label="Info">ⓘ</Button> </MyTreeItemContent> <TreeItem id="report" textValue="Weekly Report"> <MyTreeItemContent> Weekly Report <Button aria-label="Info">ⓘ</Button> </MyTreeItemContent> </TreeItem> </TreeItem> </TreeItem> <TreeItem id="photos" textValue="Photos"> <MyTreeItemContent> Photos <Button aria-label="Info">ⓘ</Button> </MyTreeItemContent> <TreeItem id="one" textValue="Image 1"> <MyTreeItemContent> Image 1 <Button aria-label="Info">ⓘ</Button> </MyTreeItemContent> </TreeItem> <TreeItem id="two" textValue="Image 2"> <MyTreeItemContent> Image 2 <Button aria-label="Info">ⓘ</Button> </MyTreeItemContent> </TreeItem> </TreeItem> </Tree> import { Button, Collection, Tree, TreeItem, TreeItemContent } from 'react-aria-components'; <Tree aria-label="Files" style={{ height: '300px' }} defaultExpandedKeys={[ 'documents', 'photos', 'project' ]} selectionMode="multiple" defaultSelectedKeys={[ 'photos' ]} > <TreeItem id="documents" textValue="Documents" > <MyTreeItemContent> Documents <Button aria-label="Info"> ⓘ </Button> </MyTreeItemContent> <TreeItem id="project" textValue="Project" > <MyTreeItemContent> Project <Button aria-label="Info"> ⓘ </Button> </MyTreeItemContent> <TreeItem id="report" textValue="Weekly Report" > <MyTreeItemContent> Weekly Report <Button aria-label="Info"> ⓘ </Button> </MyTreeItemContent> </TreeItem> </TreeItem> </TreeItem> <TreeItem id="photos" textValue="Photos" > <MyTreeItemContent> Photos <Button aria-label="Info"> ⓘ </Button> </MyTreeItemContent> <TreeItem id="one" textValue="Image 1" > <MyTreeItemContent> Image 1 <Button aria-label="Info"> ⓘ </Button> </MyTreeItemContent> </TreeItem> <TreeItem id="two" textValue="Image 2" > <MyTreeItemContent> Image 2 <Button aria-label="Info"> ⓘ </Button> </MyTreeItemContent> </TreeItem> </TreeItem> </Tree> Documentsⓘ Projectⓘ Weekly Reportⓘ Photosⓘ Image 1ⓘ Image 2ⓘ Show CSS .react-aria-Tree { display: flex; flex-direction: column; gap: 2px; overflow: auto; padding: 4px; border: 1px solid var(--border-color); border-radius: 6px; background: var(--overlay-background); forced-color-adjust: none; outline: none; width: 250px; max-height: 300px; box-sizing: border-box; &[data-focus-visible] { outline: 2px solid var(--focus-ring-color); outline-offset: -1px; } .react-aria-TreeItem { display: flex; align-items: center; gap: 0.571rem; min-height: 28px; padding: 0.286rem 0.286rem 0.286rem 0.571rem; --padding: 16px; border-radius: 6px; outline: none; cursor: default; color: var(--text-color); font-size: 1.072rem; position: relative; transform: translateZ(0); .react-aria-Button[slot=chevron] { all: unset; display: flex; visibility: hidden; align-items: center; justify-content: center; width: 1.3rem; height: 100%; padding-left: calc((var(--tree-item-level) - 1) * var(--padding)); svg { rotate: 0deg; transition: rotate 200ms; width: 12px; height: 12px; fill: none; stroke: currentColor; stroke-width: 3px; } } &[data-has-child-items] .react-aria-Button[slot=chevron] { visibility: visible; } &[data-expanded] .react-aria-Button[slot=chevron] svg { rotate: 90deg; } &[data-focus-visible] { outline: 2px solid var(--focus-ring-color); outline-offset: -2px; } &[data-pressed] { background: var(--gray-100); } &[data-selected] { background: var(--highlight-background); color: var(--highlight-foreground); --focus-ring-color: var(--highlight-foreground); &[data-focus-visible] { outline-color: var(--highlight-foreground); outline-offset: -4px; } .react-aria-Button { color: var(--highlight-foreground); --highlight-hover: rgb(255 255 255 / 0.1); --highlight-pressed: rgb(255 255 255 / 0.2); } } &[data-disabled] { color: var(--text-color-disabled); } .react-aria-Button:not([slot]) { margin-left: auto; background: transparent; border: none; font-size: 1.2rem; line-height: 1.2em; padding: 0.286rem 0.429rem; transition: background 200ms; &[data-hovered] { background: var(--highlight-hover); } &[data-pressed] { background: var(--highlight-pressed); box-shadow: none; } } } /* join selected items if :has selector is supported */ @supports selector(:has(.foo)) { gap: 0; .react-aria-TreeItem[data-selected]:has(+ [data-selected]) { border-end-start-radius: 0; border-end-end-radius: 0; } .react-aria-TreeItem[data-selected] + [data-selected] { border-start-start-radius: 0; border-start-end-radius: 0; } } :where(.react-aria-TreeItem) .react-aria-Checkbox { --selected-color: var(--highlight-foreground); --selected-color-pressed: var(--highlight-foreground-pressed); --checkmark-color: var(--highlight-background); --background-color: var(--highlight-background); } } .react-aria-Tree { display: flex; flex-direction: column; gap: 2px; overflow: auto; padding: 4px; border: 1px solid var(--border-color); border-radius: 6px; background: var(--overlay-background); forced-color-adjust: none; outline: none; width: 250px; max-height: 300px; box-sizing: border-box; &[data-focus-visible] { outline: 2px solid var(--focus-ring-color); outline-offset: -1px; } .react-aria-TreeItem { display: flex; align-items: center; gap: 0.571rem; min-height: 28px; padding: 0.286rem 0.286rem 0.286rem 0.571rem; --padding: 16px; border-radius: 6px; outline: none; cursor: default; color: var(--text-color); font-size: 1.072rem; position: relative; transform: translateZ(0); .react-aria-Button[slot=chevron] { all: unset; display: flex; visibility: hidden; align-items: center; justify-content: center; width: 1.3rem; height: 100%; padding-left: calc((var(--tree-item-level) - 1) * var(--padding)); svg { rotate: 0deg; transition: rotate 200ms; width: 12px; height: 12px; fill: none; stroke: currentColor; stroke-width: 3px; } } &[data-has-child-items] .react-aria-Button[slot=chevron] { visibility: visible; } &[data-expanded] .react-aria-Button[slot=chevron] svg { rotate: 90deg; } &[data-focus-visible] { outline: 2px solid var(--focus-ring-color); outline-offset: -2px; } &[data-pressed] { background: var(--gray-100); } &[data-selected] { background: var(--highlight-background); color: var(--highlight-foreground); --focus-ring-color: var(--highlight-foreground); &[data-focus-visible] { outline-color: var(--highlight-foreground); outline-offset: -4px; } .react-aria-Button { color: var(--highlight-foreground); --highlight-hover: rgb(255 255 255 / 0.1); --highlight-pressed: rgb(255 255 255 / 0.2); } } &[data-disabled] { color: var(--text-color-disabled); } .react-aria-Button:not([slot]) { margin-left: auto; background: transparent; border: none; font-size: 1.2rem; line-height: 1.2em; padding: 0.286rem 0.429rem; transition: background 200ms; &[data-hovered] { background: var(--highlight-hover); } &[data-pressed] { background: var(--highlight-pressed); box-shadow: none; } } } /* join selected items if :has selector is supported */ @supports selector(:has(.foo)) { gap: 0; .react-aria-TreeItem[data-selected]:has(+ [data-selected]) { border-end-start-radius: 0; border-end-end-radius: 0; } .react-aria-TreeItem[data-selected] + [data-selected] { border-start-start-radius: 0; border-start-end-radius: 0; } } :where(.react-aria-TreeItem) .react-aria-Checkbox { --selected-color: var(--highlight-foreground); --selected-color-pressed: var(--highlight-foreground-pressed); --checkmark-color: var(--highlight-background); --background-color: var(--highlight-background); } } .react-aria-Tree { display: flex; flex-direction: column; gap: 2px; overflow: auto; padding: 4px; border: 1px solid var(--border-color); border-radius: 6px; background: var(--overlay-background); forced-color-adjust: none; outline: none; width: 250px; max-height: 300px; box-sizing: border-box; &[data-focus-visible] { outline: 2px solid var(--focus-ring-color); outline-offset: -1px; } .react-aria-TreeItem { display: flex; align-items: center; gap: 0.571rem; min-height: 28px; padding: 0.286rem 0.286rem 0.286rem 0.571rem; --padding: 16px; border-radius: 6px; outline: none; cursor: default; color: var(--text-color); font-size: 1.072rem; position: relative; transform: translateZ(0); .react-aria-Button[slot=chevron] { all: unset; display: flex; visibility: hidden; align-items: center; justify-content: center; width: 1.3rem; height: 100%; padding-left: calc((var(--tree-item-level) - 1) * var(--padding)); svg { rotate: 0deg; transition: rotate 200ms; width: 12px; height: 12px; fill: none; stroke: currentColor; stroke-width: 3px; } } &[data-has-child-items] .react-aria-Button[slot=chevron] { visibility: visible; } &[data-expanded] .react-aria-Button[slot=chevron] svg { rotate: 90deg; } &[data-focus-visible] { outline: 2px solid var(--focus-ring-color); outline-offset: -2px; } &[data-pressed] { background: var(--gray-100); } &[data-selected] { background: var(--highlight-background); color: var(--highlight-foreground); --focus-ring-color: var(--highlight-foreground); &[data-focus-visible] { outline-color: var(--highlight-foreground); outline-offset: -4px; } .react-aria-Button { color: var(--highlight-foreground); --highlight-hover: rgb(255 255 255 / 0.1); --highlight-pressed: rgb(255 255 255 / 0.2); } } &[data-disabled] { color: var(--text-color-disabled); } .react-aria-Button:not([slot]) { margin-left: auto; background: transparent; border: none; font-size: 1.2rem; line-height: 1.2em; padding: 0.286rem 0.429rem; transition: background 200ms; &[data-hovered] { background: var(--highlight-hover); } &[data-pressed] { background: var(--highlight-pressed); box-shadow: none; } } } /* join selected items if :has selector is supported */ @supports selector(:has(.foo)) { gap: 0; .react-aria-TreeItem[data-selected]:has(+ [data-selected]) { border-end-start-radius: 0; border-end-end-radius: 0; } .react-aria-TreeItem[data-selected] + [data-selected] { border-start-start-radius: 0; border-start-end-radius: 0; } } :where(.react-aria-TreeItem) .react-aria-Checkbox { --selected-color: var(--highlight-foreground); --selected-color-pressed: var(--highlight-foreground-pressed); --checkmark-color: var(--highlight-background); --background-color: var(--highlight-background); } } ## Features# * * * A tree can be built using the <ul>, <li>, and <ol>, but is very limited in functionality especially when it comes to user interactions. HTML lists are meant for static content, rather than hierarchies with rich interactions like focusable elements within cells, keyboard navigation, item selection, sorting, etc. `Tree` helps achieve accessible and interactive tree components that can be styled as needed. * **Item selection** – Single or multiple selection, with optional checkboxes, disabled items, and both `toggle` and `replace` selection behaviors. * **Interactive children** – Tree items may include interactive elements such as buttons, menus, etc. * **Actions** – Items support optional actions such as navigation via click, tap, double click, or Enter key. * **Keyboard navigation** – Tree items and focusable children can be navigated using the arrow keys, along with page up/down, home/end, etc. Typeahead, auto scrolling, and selection modifier keys are supported as well. * **Drag and drop** – Tree supports drag and drop to reorder, move, insert, or update items via mouse, touch, keyboard, and screen reader interactions. * **Virtualized scrolling** – Use Virtualizer to improve performance of large lists by rendering only the visible items. * **Touch friendly** – Selection and actions adapt their behavior depending on the device. For example, selection is activated via long press on touch when item actions are present. * **Accessible** – Follows the ARIA treegrid pattern, with additional selection announcements via an ARIA live region. Extensively tested across many devices and assistive technologies to ensure announcements and behaviors are consistent. ## Anatomy# * * * A Tree consists of a container element, with items containing data inside. The items within a tree may contain focusable elements or plain text content. Each item may also contain a button to toggle the expandable state of that item. If the tree supports item selection, each item can optionally include a selection checkbox. import {Button, Checkbox, Tree, TreeItem, TreeItemContent} from 'react-aria-components'; <Tree> <TreeItem> <TreeItemContent> <Button slot="chevron" /> <Checkbox slot="selection" /> <Button slot="drag" /> </TreeItemContent> <TreeItem> {/* ... */} </TreeItem> </TreeItem> </Tree> import { Button, Checkbox, Tree, TreeItem, TreeItemContent } from 'react-aria-components'; <Tree> <TreeItem> <TreeItemContent> <Button slot="chevron" /> <Checkbox slot="selection" /> <Button slot="drag" /> </TreeItemContent> <TreeItem> {/* ... */} </TreeItem> </TreeItem> </Tree> import { Button, Checkbox, Tree, TreeItem, TreeItemContent } from 'react-aria-components'; <Tree> <TreeItem> <TreeItemContent> <Button slot="chevron" /> <Checkbox slot="selection" /> <Button slot="drag" /> </TreeItemContent> <TreeItem> {/* ... */} </TreeItem> </TreeItem> </Tree> ### Concepts# `Tree` makes use of the following concepts: Collections Defining collections of items, async loading, and updating items over time. Selection Interactions and data structures to represent selection. Drag and drop Concepts and interactions for an accessible drag and drop experience. ### Composed components# A `Tree` uses the following components, which may also be used standalone or reused in other components. Checkbox A checkbox allows a user to select an individual option. Button A button allows a user to perform an action. ## Examples# * * * File System Tree A tree with multiple selection and nested items. ## Starter kits# * * * To help kick-start your project, we offer starter kits that include example implementations of all React Aria components with various styling solutions. All components are fully styled, including support for dark mode, high contrast mode, and all UI states. Each starter comes with a pre-configured Storybook that you can experiment with, or use as a starting point for your own component library. Vanilla CSS Download ZIP Preview Tailwind CSS Download ZIP Preview ## Reusable wrappers# * * * If you will use a Tree in multiple places in your app, you can wrap all of the pieces into a reusable component. This way, the DOM structure, styling code, and other logic are defined in a single place and reused everywhere to ensure consistency. import type {TreeItemContentProps, TreeItemContentRenderProps} from 'react-aria-components'; import {Button} from 'react-aria-components'; function MyTreeItemContent( props: Omit<TreeItemContentProps, 'children'> & { children?: React.ReactNode } ) { return ( <TreeItemContent> {( { hasChildItems, selectionBehavior, selectionMode, allowsDragging }: TreeItemContentRenderProps ) => ( <> {allowsDragging && <Button slot="drag">≡</Button>} {selectionBehavior === 'toggle' && selectionMode !== 'none' && ( <MyCheckbox slot="selection" /> )} <Button slot="chevron"> <svg viewBox="0 0 24 24"> <path d="m8.25 4.5 7.5 7.5-7.5 7.5" /> </svg> </Button> {props.children} </> )} </TreeItemContent> ); } import type { TreeItemContentProps, TreeItemContentRenderProps } from 'react-aria-components'; import {Button} from 'react-aria-components'; function MyTreeItemContent( props: Omit<TreeItemContentProps, 'children'> & { children?: React.ReactNode; } ) { return ( <TreeItemContent> {( { hasChildItems, selectionBehavior, selectionMode, allowsDragging }: TreeItemContentRenderProps ) => ( <> {allowsDragging && <Button slot="drag">≡</Button>} {selectionBehavior === 'toggle' && selectionMode !== 'none' && ( <MyCheckbox slot="selection" /> )} <Button slot="chevron"> <svg viewBox="0 0 24 24"> <path d="m8.25 4.5 7.5 7.5-7.5 7.5" /> </svg> </Button> {props.children} </> )} </TreeItemContent> ); } import type { TreeItemContentProps, TreeItemContentRenderProps } from 'react-aria-components'; import {Button} from 'react-aria-components'; function MyTreeItemContent( props: & Omit< TreeItemContentProps, 'children' > & { children?: React.ReactNode; } ) { return ( <TreeItemContent> {({ hasChildItems, selectionBehavior, selectionMode, allowsDragging }: TreeItemContentRenderProps) => ( <> {allowsDragging && ( <Button slot="drag"> ≡ </Button> )} {selectionBehavior === 'toggle' && selectionMode !== 'none' && ( <MyCheckbox slot="selection" /> )} <Button slot="chevron"> <svg viewBox="0 0 24 24"> <path d="m8.25 4.5 7.5 7.5-7.5 7.5" /> </svg> </Button> {props .children} </> )} </TreeItemContent> ); } The `TreeItem` can also be wrapped. This example accepts a `title` prop and renders the `TreeItemContent` automatically. import {TreeItemProps} from 'react-aria-components'; interface MyTreeItemProps extends Partial<TreeItemProps> { title: string } function MyTreeItem(props: MyTreeItemProps) { return ( <TreeItem textValue={props.title} {...props}> <MyTreeItemContent> {props.title} </MyTreeItemContent> {props.children} </TreeItem> ); } import {TreeItemProps} from 'react-aria-components'; interface MyTreeItemProps extends Partial<TreeItemProps> { title: string } function MyTreeItem(props: MyTreeItemProps) { return ( <TreeItem textValue={props.title} {...props}> <MyTreeItemContent> {props.title} </MyTreeItemContent> {props.children} </TreeItem> ); } import {TreeItemProps} from 'react-aria-components'; interface MyTreeItemProps extends Partial< TreeItemProps > { title: string; } function MyTreeItem( props: MyTreeItemProps ) { return ( <TreeItem textValue={props .title} {...props} > <MyTreeItemContent> {props.title} </MyTreeItemContent> {props.children} </TreeItem> ); } Now we can render a Tree using far less code. <Tree aria-label="Files" style={{ height: '300px' }} defaultExpandedKeys={['documents', 'photos', 'project']} > <MyTreeItem title="Documents"> <MyTreeItem title="Project"> <MyTreeItem title="Weekly Report" /> </MyTreeItem> </MyTreeItem> <MyTreeItem title="Photos"> <MyTreeItem title="Image 1" /> <MyTreeItem title="Image 2" /> </MyTreeItem> </Tree> <Tree aria-label="Files" style={{ height: '300px' }} defaultExpandedKeys={['documents', 'photos', 'project']} > <MyTreeItem title="Documents"> <MyTreeItem title="Project"> <MyTreeItem title="Weekly Report" /> </MyTreeItem> </MyTreeItem> <MyTreeItem title="Photos"> <MyTreeItem title="Image 1" /> <MyTreeItem title="Image 2" /> </MyTreeItem> </Tree> <Tree aria-label="Files" style={{ height: '300px' }} defaultExpandedKeys={[ 'documents', 'photos', 'project' ]} > <MyTreeItem title="Documents"> <MyTreeItem title="Project"> <MyTreeItem title="Weekly Report" /> </MyTreeItem> </MyTreeItem> <MyTreeItem title="Photos"> <MyTreeItem title="Image 1" /> <MyTreeItem title="Image 2" /> </MyTreeItem> </Tree> Documents Photos ## Content# * * * So far, our examples have shown static collections where the data is hard coded. Dynamic collections, as shown below, can be used when the tree data comes from an external data source such as an API, or updates over time. In the example below, data for each item is provided to the tree via a render function. import type {TreeProps} from 'react-aria-components'; import {MyCheckbox} from './Checkbox'; let items = [ {id: '1', title: 'Documents', type: 'directory', children: [ {id: '2', title: 'Project', type: 'directory', children: [ {id: '3', title: 'Weekly Report', type: 'file', children: []}, {id: '4', title: 'Budget', type: 'file', children: []} ]} ]}, {id: '5', title: 'Photos', type: 'directory', children: [ {id: '6', title: 'Image 1', type: 'file', children: []}, {id: '7', title: 'Image 2', type: 'file', children: []} ]} ]; interface FileType { id: string, title: string, type: 'directory' | 'file', children: FileType[] } function FileTree(props: TreeProps<FileType>) { return ( <Tree aria-label="Files" defaultExpandedKeys={[1, 4]} items={items} selectionMode="multiple" {...props}> {function renderItem(item) { return ( <TreeItem textValue={item.title}> <MyTreeItemContent> {item.title} <Button aria-label="Info" onPress={() => alert(`Info for ${item.title}...`)}> ⓘ </Button> </MyTreeItemContent> <Collection items={item.children}> {/* recursively render children */} {renderItem} </Collection> </TreeItem> ); }} </Tree> ) } import type {TreeProps} from 'react-aria-components'; import {MyCheckbox} from './Checkbox'; let items = [ { id: '1', title: 'Documents', type: 'directory', children: [ { id: '2', title: 'Project', type: 'directory', children: [ { id: '3', title: 'Weekly Report', type: 'file', children: [] }, { id: '4', title: 'Budget', type: 'file', children: [] } ] } ] }, { id: '5', title: 'Photos', type: 'directory', children: [ { id: '6', title: 'Image 1', type: 'file', children: [] }, { id: '7', title: 'Image 2', type: 'file', children: [] } ] } ]; interface FileType { id: string; title: string; type: 'directory' | 'file'; children: FileType[]; } function FileTree(props: TreeProps<FileType>) { return ( <Tree aria-label="Files" defaultExpandedKeys={[1, 4]} items={items} selectionMode="multiple" {...props} > {function renderItem(item) { return ( <TreeItem textValue={item.title}> <MyTreeItemContent> {item.title} <Button aria-label="Info" onPress={() => alert(`Info for ${item.title}...`)} > ⓘ </Button> </MyTreeItemContent> <Collection items={item.children}> {/* recursively render children */} {renderItem} </Collection> </TreeItem> ); }} </Tree> ); } import type {TreeProps} from 'react-aria-components'; import {MyCheckbox} from './Checkbox'; let items = [ { id: '1', title: 'Documents', type: 'directory', children: [ { id: '2', title: 'Project', type: 'directory', children: [ { id: '3', title: 'Weekly Report', type: 'file', children: [] }, { id: '4', title: 'Budget', type: 'file', children: [] } ] } ] }, { id: '5', title: 'Photos', type: 'directory', children: [ { id: '6', title: 'Image 1', type: 'file', children: [] }, { id: '7', title: 'Image 2', type: 'file', children: [] } ] } ]; interface FileType { id: string; title: string; type: | 'directory' | 'file'; children: FileType[]; } function FileTree( props: TreeProps< FileType > ) { return ( <Tree aria-label="Files" defaultExpandedKeys={[ 1, 4 ]} items={items} selectionMode="multiple" {...props} > {function renderItem( item ) { return ( <TreeItem textValue={item .title} > <MyTreeItemContent> {item .title} <Button aria-label="Info" onPress={() => alert( `Info for ${item.title}...` )} > ⓘ </Button> </MyTreeItemContent> <Collection items={item .children} > {/* recursively render children */} {renderItem} </Collection> </TreeItem> ); }} </Tree> ); } Documentsⓘ Photosⓘ ## Selection# * * * ### Single selection# By default, `Tree` doesn't allow item selection but this can be enabled using the `selectionMode` prop. Use `defaultSelectedKeys` to provide a default set of selected items. Note that the value of the selected keys must match the `id` prop of the item. The example below enables single selection mode and uses `defaultSelectedKeys` to select the item with id equal to `2`. A user can click on a different item to change the selection or click on the same item again to deselect it entirely. // Using the example above <FileTree selectionMode="single" defaultSelectedKeys={[2]} defaultExpandedKeys={[1]} /> // Using the example above <FileTree selectionMode="single" defaultSelectedKeys={[2]} defaultExpandedKeys={[1]} /> // Using the example above <FileTree selectionMode="single" defaultSelectedKeys={[ 2 ]} defaultExpandedKeys={[ 1 ]} /> Documentsⓘ Photosⓘ ### Multiple selection# Multiple selection can be enabled by setting `selectionMode` to `multiple`. // Using the example above <FileTree selectionMode="multiple" defaultSelectedKeys={[2, 4]} defaultExpandedKeys={[1]} /> // Using the example above <FileTree selectionMode="multiple" defaultSelectedKeys={[2, 4]} defaultExpandedKeys={[1]} /> // Using the example above <FileTree selectionMode="multiple" defaultSelectedKeys={[ 2, 4 ]} defaultExpandedKeys={[ 1 ]} /> Documentsⓘ Photosⓘ ### Disallow empty selection# Tree also supports a `disallowEmptySelection` prop which forces the user to have at least one item in the Tree selected at all times. In this mode, if a single item is selected and the user presses it, it will not be deselected. // Using the example above <FileTree selectionMode="single" defaultSelectedKeys={[2]} defaultExpandedKeys={[1]} disallowEmptySelection /> // Using the example above <FileTree selectionMode="single" defaultSelectedKeys={[2]} defaultExpandedKeys={[1]} disallowEmptySelection /> // Using the example above <FileTree selectionMode="single" defaultSelectedKeys={[ 2 ]} defaultExpandedKeys={[ 1 ]} disallowEmptySelection /> Documentsⓘ Photosⓘ ### Controlled selection# To programmatically control item selection, use the `selectedKeys` prop paired with the `onSelectionChange` callback. The `id` prop from the selected items will be passed into the callback when the item is pressed, allowing you to update state accordingly. import type {Selection} from 'react-aria-components'; interface Pokemon { id: number, name: string, children?: Pokemon[] } interface PokemonEvolutionTreeProps<T> extends TreeProps<T> { items?: T[], renderEmptyState?: () => string } function PokemonEvolutionTree( props: PokemonEvolutionTreeProps<Pokemon> ) { let items: Pokemon[] = props.items ?? [ {id: 1, name: 'Bulbasaur', children: [ {id: 2, name: 'Ivysaur', children: [ {id: 3, name: 'Venusaur'} ]} ]}, {id: 4, name: 'Charmander', children: [ {id: 5, name: 'Charmeleon', children: [ {id: 6, name: 'Charizard'} ]} ]}, {id: 7, name: 'Squirtle', children: [ {id: 8, name: 'Wartortle', children: [ {id: 9, name: 'Blastoise'} ]} ]} ]; let [selectedKeys, setSelectedKeys] = React.useState<Selection>(new Set()); return ( <Tree aria-label="Pokemon evolution tree" style={{height: '300px'}} items={items} defaultExpandedKeys={[1, 2]} selectedKeys={selectedKeys} onSelectionChange={setSelectedKeys} {...props} > {function renderItem(item) { return ( <MyTreeItem title={item.name}> <Collection items={item.children}> {renderItem} </Collection> </MyTreeItem> ); }} </Tree> ); } <PokemonEvolutionTree selectionMode="multiple" /> import type {Selection} from 'react-aria-components'; interface Pokemon { id: number; name: string; children?: Pokemon[]; } interface PokemonEvolutionTreeProps<T> extends TreeProps<T> { items?: T[]; renderEmptyState?: () => string; } function PokemonEvolutionTree( props: PokemonEvolutionTreeProps<Pokemon> ) { let items: Pokemon[] = props.items ?? [ { id: 1, name: 'Bulbasaur', children: [ { id: 2, name: 'Ivysaur', children: [ { id: 3, name: 'Venusaur' } ] } ] }, { id: 4, name: 'Charmander', children: [ { id: 5, name: 'Charmeleon', children: [ { id: 6, name: 'Charizard' } ] } ] }, { id: 7, name: 'Squirtle', children: [ { id: 8, name: 'Wartortle', children: [ { id: 9, name: 'Blastoise' } ] } ] } ]; let [selectedKeys, setSelectedKeys] = React.useState< Selection >(new Set()); return ( <Tree aria-label="Pokemon evolution tree" style={{ height: '300px' }} items={items} defaultExpandedKeys={[1, 2]} selectedKeys={selectedKeys} onSelectionChange={setSelectedKeys} ...props} > {function renderItem(item) { return ( <MyTreeItem title={item.name}> <Collection items={item.children}> {renderItem} </Collection> </MyTreeItem> ); }} </Tree> ); } <PokemonEvolutionTree selectionMode="multiple" /> import type {Selection} from 'react-aria-components'; interface Pokemon { id: number; name: string; children?: Pokemon[]; } interface PokemonEvolutionTreeProps< T > extends TreeProps<T> { items?: T[]; renderEmptyState?: () => string; } function PokemonEvolutionTree( props: PokemonEvolutionTreeProps< Pokemon > ) { let items: Pokemon[] = props.items ?? [ { id: 1, name: 'Bulbasaur', children: [ { id: 2, name: 'Ivysaur', children: [ { id: 3, name: 'Venusaur' } ] } ] }, { id: 4, name: 'Charmander', children: [ { id: 5, name: 'Charmeleon', children: [ { id: 6, name: 'Charizard' } ] } ] }, { id: 7, name: 'Squirtle', children: [ { id: 8, name: 'Wartortle', children: [ { id: 9, name: 'Blastoise' } ] } ] } ]; let [ selectedKeys, setSelectedKeys ] = React.useState< Selection >(new Set()); return ( <Tree aria-label="Pokemon evolution tree" style={{ height: '300px' }} items={items} defaultExpandedKeys={[ 1, 2 ]} selectedKeys={selectedKeys} onSelectionChange={setSelectedKeys} ...props} > {function renderItem( item ) { return ( <MyTreeItem title={item .name} > <Collection items={item .children} > {renderItem} </Collection> </MyTreeItem> ); }} </Tree> ); } <PokemonEvolutionTree selectionMode="multiple" /> Bulbasaur Ivysaur Venusaur Charmander Squirtle ### Selection behavior# By default, `Tree` uses the `"toggle"` selection behavior, which behaves like a checkbox group: clicking, tapping, or pressing the Space or Enter keys toggles selection for the focused item. Using the arrow keys moves focus but does not change selection. The `"toggle"` selection mode is often paired with checkboxes in each item as an explicit affordance for selection. When the `selectionBehavior` prop is set to `"replace"`, clicking an item with the mouse _replaces_ the selection with only that item. Using the arrow keys moves both focus and selection. To select multiple items, modifier keys such as Ctrl, Cmd, and Shift can be used. To move focus without moving selection, the Ctrl key on Windows or the Option key on macOS can be held while pressing the arrow keys. Holding this modifier while pressing the Space key toggles selection for the focused item, which allows multiple selection of non-contiguous items. On touch screen devices, selection always behaves as toggle since modifier keys may not be available. This behavior emulates native platforms such as macOS and Windows and is often used when checkboxes in each item are not desired. <PokemonEvolutionTree selectionMode="multiple" selectionBehavior="replace" /> <PokemonEvolutionTree selectionMode="multiple" selectionBehavior="replace" /> <PokemonEvolutionTree selectionMode="multiple" selectionBehavior="replace" /> Bulbasaur Ivysaur Venusaur Charmander Squirtle ## Item actions# * * * `Tree` supports item actions via the `onAction` prop, which is useful for functionality such as navigation. In the default `"toggle"` selection behavior, when nothing is selected, clicking or tapping the item triggers the item action. When at least one item is selected, the tree is in selection mode, and clicking or tapping an item toggles the selection. Actions may also be triggered via the Enter key, and selection using the Space key. This behavior is slightly different in the `"replace"` selection behavior, where single clicking selects the item and actions are performed via double click. On touch devices, the action becomes the primary tap interaction, and a long press enters into selection mode, which temporarily swaps the selection behavior to `"toggle"` to perform selection (you may wish to display checkboxes when this happens). Deselecting all items exits selection mode and reverts the selection behavior back to `"replace"`. Keyboard behaviors are unaffected. <div style={{display: 'flex', flexWrap: 'wrap', gap: '24px'}}> <PokemonEvolutionTree aria-label="Pokemon tree with item actions and toggle selection behavior" onAction={key => alert(`Opening item ${key}...`)} selectionMode="multiple" /> <PokemonEvolutionTree aria-label="Pokemon tree with item actions and replace selection behavior" onAction={key => alert(`Opening item ${key}...`)} selectionBehavior="replace" selectionMode="multiple" /> </div> <div style={{ display: 'flex', flexWrap: 'wrap', gap: '24px' }} > <PokemonEvolutionTree aria-label="Pokemon tree with item actions and toggle selection behavior" onAction={(key) => alert(`Opening item ${key}...`)} selectionMode="multiple" /> <PokemonEvolutionTree aria-label="Pokemon tree with item actions and replace selection behavior" onAction={(key) => alert(`Opening item ${key}...`)} selectionBehavior="replace" selectionMode="multiple" /> </div> <div style={{ display: 'flex', flexWrap: 'wrap', gap: '24px' }} > <PokemonEvolutionTree aria-label="Pokemon tree with item actions and toggle selection behavior" onAction={(key) => alert( `Opening item ${key}...` )} selectionMode="multiple" /> <PokemonEvolutionTree aria-label="Pokemon tree with item actions and replace selection behavior" onAction={(key) => alert( `Opening item ${key}...` )} selectionBehavior="replace" selectionMode="multiple" /> </div> Bulbasaur Ivysaur Venusaur Charmander Squirtle Bulbasaur Ivysaur Venusaur Charmander Squirtle Items may also have an action specified by directly applying `onAction` on the `TreeItem` itself. This may be especially convenient in static collections. If `onAction` is also provided to the `Tree`, both the tree's and the item's `onAction` are called. <Tree aria-label="Tree with onAction applied on the items directly" style={{ height: '300px' }} defaultExpandedKeys={['bulbasaur', 'ivysaur']} > <MyTreeItem onAction={() => alert(`Opening Bulbasaur...`)} id="bulbasaur" title="Bulbasaur" > <MyTreeItem onAction={() => alert(`Opening Ivysaur...`)} id="ivysaur" title="Ivysaur" > <MyTreeItem onAction={() => alert(`Opening Venusaur...`)} id="venusaur" title="Venusaur" /> </MyTreeItem> </MyTreeItem> </Tree> <Tree aria-label="Tree with onAction applied on the items directly" style={{ height: '300px' }} defaultExpandedKeys={['bulbasaur', 'ivysaur']} > <MyTreeItem onAction={() => alert(`Opening Bulbasaur...`)} id="bulbasaur" title="Bulbasaur" > <MyTreeItem onAction={() => alert(`Opening Ivysaur...`)} id="ivysaur" title="Ivysaur" > <MyTreeItem onAction={() => alert(`Opening Venusaur...`)} id="venusaur" title="Venusaur" /> </MyTreeItem> </MyTreeItem> </Tree> <Tree aria-label="Tree with onAction applied on the items directly" style={{ height: '300px' }} defaultExpandedKeys={[ 'bulbasaur', 'ivysaur' ]} > <MyTreeItem onAction={() => alert( `Opening Bulbasaur...` )} id="bulbasaur" title="Bulbasaur" > <MyTreeItem onAction={() => alert( `Opening Ivysaur...` )} id="ivysaur" title="Ivysaur" > <MyTreeItem onAction={() => alert( `Opening Venusaur...` )} id="venusaur" title="Venusaur" /> </MyTreeItem> </MyTreeItem> </Tree> Bulbasaur Ivysaur Venusaur ### Links# Tree items may also be links to another page or website. This can be achieved by passing the `href` prop to the `<TreeItem>` component. Links behave the same way as described above for item actions depending on the `selectionMode` and `selectionBehavior`. <Tree aria-label="Tree with onAction applied on the items directly" style={{ height: '200px' }} defaultExpandedKeys={['bulbasaur', 'ivysaur']} > <MyTreeItem href="https://pokemondb.net/pokedex/bulbasaur" target="_blank" id="bulbasaur" title="Bulbasaur" > <MyTreeItem id="ivysaur" title="Ivysaur" href="https://pokemondb.net/pokedex/ivysaur" target="_blank" > <MyTreeItem id="venusaur" title="Venusaur" href="https://pokemondb.net/pokedex/venusaur" target="_blank" /> </MyTreeItem> </MyTreeItem> </Tree> <Tree aria-label="Tree with onAction applied on the items directly" style={{ height: '200px' }} defaultExpandedKeys={['bulbasaur', 'ivysaur']} > <MyTreeItem href="https://pokemondb.net/pokedex/bulbasaur" target="_blank" id="bulbasaur" title="Bulbasaur" > <MyTreeItem id="ivysaur" title="Ivysaur" href="https://pokemondb.net/pokedex/ivysaur" target="_blank" > <MyTreeItem id="venusaur" title="Venusaur" href="https://pokemondb.net/pokedex/venusaur" target="_blank" /> </MyTreeItem> </MyTreeItem> </Tree> <Tree aria-label="Tree with onAction applied on the items directly" style={{ height: '200px' }} defaultExpandedKeys={[ 'bulbasaur', 'ivysaur' ]} > <MyTreeItem href="https://pokemondb.net/pokedex/bulbasaur" target="_blank" id="bulbasaur" title="Bulbasaur" > <MyTreeItem id="ivysaur" title="Ivysaur" href="https://pokemondb.net/pokedex/ivysaur" target="_blank" > <MyTreeItem id="venusaur" title="Venusaur" href="https://pokemondb.net/pokedex/venusaur" target="_blank" /> </MyTreeItem> </MyTreeItem> </Tree> Bulbasaur Ivysaur Venusaur #### Client side routing# The `<TreeItem>` component works with frameworks and client side routers like Next.js and React Router. As with other React Aria components that support links, this works via the `RouterProvider` component at the root of your app. See the client side routing guide to learn how to set this up. ## Disabled items# * * * A `TreeItem` can be disabled with the `isDisabled` prop. This will disable all interactions on the item unless the `disabledBehavior` prop on `Tree` is used to change this behavior. Note that you are responsible for the styling of disabled items, however, the selection checkbox will be automatically disabled. <Tree aria-label="Tree with disabled items" style={{ height: '100px' }} defaultExpandedKeys={['bulbasaur']} > <MyTreeItem id="bulbasaur" title="Bulbasaur"> <MyTreeItem id="ivysaur" title="Ivysaur" isDisabled> <MyTreeItem id="venusaur" title="Venusaur" /> </MyTreeItem> </MyTreeItem> </Tree> <Tree aria-label="Tree with disabled items" style={{ height: '100px' }} defaultExpandedKeys={['bulbasaur']} > <MyTreeItem id="bulbasaur" title="Bulbasaur"> <MyTreeItem id="ivysaur" title="Ivysaur" isDisabled> <MyTreeItem id="venusaur" title="Venusaur" /> </MyTreeItem> </MyTreeItem> </Tree> <Tree aria-label="Tree with disabled items" style={{ height: '100px' }} defaultExpandedKeys={[ 'bulbasaur' ]} > <MyTreeItem id="bulbasaur" title="Bulbasaur" > <MyTreeItem id="ivysaur" title="Ivysaur" isDisabled > <MyTreeItem id="venusaur" title="Venusaur" /> </MyTreeItem> </MyTreeItem> </Tree> Bulbasaur Ivysaur When `disabledBehavior` is set to `selection`, interactions such as focus, dragging, or actions can still be performed on disabled rows. <Tree aria-label="Tree with disabled items" style={{height: '100px'}} selectionMode="multiple" defaultExpandedKeys={['bulbasaur']} disabledBehavior="selection"> <MyTreeItem id="bulbasaur" title="Bulbasaur"> <MyTreeItem id="ivysaur" title="Ivysaur" isDisabled> <MyTreeItem id="venusaur" title="Venusaur" /> </MyTreeItem> </MyTreeItem> </Tree> <Tree aria-label="Tree with disabled items" style={{height: '100px'}} selectionMode="multiple" defaultExpandedKeys={['bulbasaur']} disabledBehavior="selection"> <MyTreeItem id="bulbasaur" title="Bulbasaur"> <MyTreeItem id="ivysaur" title="Ivysaur" isDisabled> <MyTreeItem id="venusaur" title="Venusaur" /> </MyTreeItem> </MyTreeItem> </Tree> <Tree aria-label="Tree with disabled items" style={{ height: '100px' }} selectionMode="multiple" defaultExpandedKeys={[ 'bulbasaur' ]} disabledBehavior="selection"> <MyTreeItem id="bulbasaur" title="Bulbasaur" > <MyTreeItem id="ivysaur" title="Ivysaur" isDisabled > <MyTreeItem id="venusaur" title="Venusaur" /> </MyTreeItem> </MyTreeItem> </Tree> Bulbasaur Ivysaur In dynamic collections, it may be more convenient to use the `disabledKeys` prop at the `Tree` level instead of `isDisabled` on individual items. This accepts a list of item ids that are disabled. An item is considered disabled if its key exists in `disabledKeys` or if it has `isDisabled`. // Using the same tree as above <PokemonEvolutionTree selectionMode="multiple" disabledKeys={[3]} /> // Using the same tree as above <PokemonEvolutionTree selectionMode="multiple" disabledKeys={[3]} /> // Using the same tree as above <PokemonEvolutionTree selectionMode="multiple" disabledKeys={[3]} /> Bulbasaur Ivysaur Venusaur Charmander Squirtle ## Empty state# * * * Use the `renderEmptyState` prop to customize what the `Tree` will display if there are no items. <Tree aria-label="Search results" renderEmptyState={() => 'No results found.'} style={{ height: '100px' }} > {[]} </Tree> <Tree aria-label="Search results" renderEmptyState={() => 'No results found.'} style={{ height: '100px' }} > {[]} </Tree> <Tree aria-label="Search results" renderEmptyState={() => 'No results found.'} style={{ height: '100px' }} > {[]} </Tree> No results found. Show CSS .react-aria-Tree { &[data-empty] { display: flex; align-items: center; justify-content: center; font-style: italic; } } .react-aria-Tree { &[data-empty] { display: flex; align-items: center; justify-content: center; font-style: italic; } } .react-aria-Tree { &[data-empty] { display: flex; align-items: center; justify-content: center; font-style: italic; } } ## Drag and drop beta# * * * Tree supports drag and drop interactions when the `dragAndDropHooks` prop is provided using the `useDragAndDrop` hook. Users can drop data on the tree as a whole, on individual items, insert new items between existing ones, or move items within the tree hierarchy. React Aria supports traditional mouse and touch based drag and drop, but also implements keyboard and screen reader friendly interactions. Users can press Enter on a draggable element to enter drag and drop mode. Then, they can press Tab to navigate between drop targets. A droppable collection is treated as a single drop target, so that users can easily tab past it to get to the next drop target. Within a droppable tree, keys such as ArrowDown and ArrowUp can be used to select a _drop position_, such as on an item, or between items. Items with children can be expanded or collapsed during keyboard drag and drop mode by using the ArrowRight and ArrowLeft keys, or the Option (Alt on Windows) + Enter keys. Screen reader users can expand and collapse items with children by navigating to and activating a visually hidden button that gets announced. Draggable tree items must include a focusable drag handle using a `<Button slot="drag">`. This enables keyboard and screen reader users to initiate drag and drop. The `MyTreeItemContent` component defined in the reusable wrappers section above can be extended to include this automatically when the tree allows dragging. See the drag and drop introduction to learn more. ### Moving between levels# The `onMove` event handler allows reordering items within a level, as well as moving items to a different level. It supports dropping both on and between items. The `getItems` function must also be implemented for items to become draggable. See below for more details. This uses useTreeData from React Stately to manage the tree data. Note that `useTreeData` is a convenience hook, not a requirement. You can manage your state however you wish. function Example() { let tree = useTreeData({ initialItems: items }); let { dragAndDropHooks } = useDragAndDrop({ getItems: (keys) => [...keys].map((key) => ({ 'text/plain': tree.getItem(key).value.title })), onMove(e) { if (e.target.dropPosition === 'before') { tree.moveBefore(e.target.key, e.keys); } else if (e.target.dropPosition === 'after') { tree.moveAfter(e.target.key, e.keys); } else if (e.target.dropPosition === 'on') { // Move items to become children of the target let targetNode = tree.getItem(e.target.key); if (targetNode) { let targetIndex = targetNode.children ? targetNode.children.length : 0; let keyArray = Array.from(e.keys); for (let i = 0; i < keyArray.length; i++) { tree.move(keyArray[i], e.target.key, targetIndex + i); } } } } }); return ( <Tree aria-label="Tree with hierarchical drag and drop" selectionMode="multiple" items={tree.items} dragAndDropHooks={dragAndDropHooks} > {function renderItem(item) { return ( <MyTreeItem title={item.value.title}> <Collection items={item.children}> {renderItem} </Collection> </MyTreeItem> ); }} </Tree> ); } function Example() { let tree = useTreeData({ initialItems: items }); let { dragAndDropHooks } = useDragAndDrop({ getItems: (keys) => [...keys].map((key) => ({ 'text/plain': tree.getItem(key).value.title })), onMove(e) { if (e.target.dropPosition === 'before') { tree.moveBefore(e.target.key, e.keys); } else if (e.target.dropPosition === 'after') { tree.moveAfter(e.target.key, e.keys); } else if (e.target.dropPosition === 'on') { // Move items to become children of the target let targetNode = tree.getItem(e.target.key); if (targetNode) { let targetIndex = targetNode.children ? targetNode.children.length : 0; let keyArray = Array.from(e.keys); for (let i = 0; i < keyArray.length; i++) { tree.move( keyArray[i], e.target.key, targetIndex + i ); } } } } }); return ( <Tree aria-label="Tree with hierarchical drag and drop" selectionMode="multiple" items={tree.items} dragAndDropHooks={dragAndDropHooks} > {function renderItem(item) { return ( <MyTreeItem title={item.value.title}> <Collection items={item.children}> {renderItem} </Collection> </MyTreeItem> ); }} </Tree> ); } function Example() { let tree = useTreeData( { initialItems: items } ); let { dragAndDropHooks } = useDragAndDrop({ getItems: (keys) => [...keys].map( (key) => ({ 'text/plain': tree.getItem( key ).value.title }) ), onMove(e) { if ( e.target .dropPosition === 'before' ) { tree.moveBefore( e.target.key, e.keys ); } else if ( e.target .dropPosition === 'after' ) { tree.moveAfter( e.target.key, e.keys ); } else if ( e.target .dropPosition === 'on' ) { // Move items to become children of the target let targetNode = tree.getItem( e.target.key ); if (targetNode) { let targetIndex = targetNode .children ? targetNode .children .length : 0; let keyArray = Array.from( e.keys ); for ( let i = 0; i < keyArray .length; i++ ) { tree.move( keyArray[ i ], e.target .key, targetIndex + i ); } } } } }); return ( <Tree aria-label="Tree with hierarchical drag and drop" selectionMode="multiple" items={tree.items} dragAndDropHooks={dragAndDropHooks} > {function renderItem( item ) { return ( <MyTreeItem title={item .value .title} > <Collection items={item .children} > {renderItem} </Collection> </MyTreeItem> ); }} </Tree> ); } ≡ Documents ≡ Photos ### Reordering within a level# The `onReorder` event handler allows reordering items within the same level. Unlike `onMove` it does not allow moving items to a different level. import {useTreeData} from 'react-stately'; import {Button, useDragAndDrop} from 'react-aria-components'; function Example() { let tree = useTreeData({ initialItems: items }); let { dragAndDropHooks } = useDragAndDrop({ getItems: (keys) => [...keys].map((key) => ({ 'text/plain': tree.getItem(key).value.title })), onReorder(e) { if (e.target.dropPosition === 'before') { tree.moveBefore(e.target.key, e.keys); } else if (e.target.dropPosition === 'after') { tree.moveAfter(e.target.key, e.keys); } } }); return ( <Tree aria-label="Reorderable tree" selectionMode="multiple" items={tree.items} dragAndDropHooks={dragAndDropHooks} > {function renderItem(item) { return ( <MyTreeItem title={item.value.title}> <Collection items={item.children}> {renderItem} </Collection> </MyTreeItem> ); }} </Tree> ); } import {useTreeData} from 'react-stately'; import { Button, useDragAndDrop } from 'react-aria-components'; function Example() { let tree = useTreeData({ initialItems: items }); let { dragAndDropHooks } = useDragAndDrop({ getItems: (keys) => [...keys].map((key) => ({ 'text/plain': tree.getItem(key).value.title })), onReorder(e) { if (e.target.dropPosition === 'before') { tree.moveBefore(e.target.key, e.keys); } else if (e.target.dropPosition === 'after') { tree.moveAfter(e.target.key, e.keys); } } }); return ( <Tree aria-label="Reorderable tree" selectionMode="multiple" items={tree.items} dragAndDropHooks={dragAndDropHooks} > {function renderItem(item) { return ( <MyTreeItem title={item.value.title}> <Collection items={item.children}> {renderItem} </Collection> </MyTreeItem> ); }} </Tree> ); } import {useTreeData} from 'react-stately'; import { Button, useDragAndDrop } from 'react-aria-components'; function Example() { let tree = useTreeData( { initialItems: items } ); let { dragAndDropHooks } = useDragAndDrop({ getItems: (keys) => [...keys].map( (key) => ({ 'text/plain': tree.getItem( key ).value.title }) ), onReorder(e) { if ( e.target .dropPosition === 'before' ) { tree.moveBefore( e.target.key, e.keys ); } else if ( e.target .dropPosition === 'after' ) { tree.moveAfter( e.target.key, e.keys ); } } }); return ( <Tree aria-label="Reorderable tree" selectionMode="multiple" items={tree.items} dragAndDropHooks={dragAndDropHooks} > {function renderItem( item ) { return ( <MyTreeItem title={item .value .title} > <Collection items={item .children} > {renderItem} </Collection> </MyTreeItem> ); }} </Tree> ); } ≡ Documents ≡ Photos Show CSS .react-aria-TreeItem { &[data-allows-dragging] { padding-left: 4px; } &[data-dragging] { opacity: 0.6; } &[data-drop-target] { outline: 2px solid var(--highlight-background); background: var(--highlight-overlay); } [slot=drag] { all: unset; width: 15px; text-align: center; &[data-focus-visible] { border-radius: 4px; outline: 2px solid var(--focus-ring-color); } } } .react-aria-Tree { &[data-selection-mode=multiple] { --checkbox-width: 28px; } &[data-allows-dragging] { --drag-button-width: 23px; } .react-aria-DropIndicator { &[data-drop-target] { outline: 1px solid var(--highlight-background); margin-left: calc(8px + var(--checkbox-width, 0px) + var(--drag-button-width, 0px) + 26px + (var(--tree-item-level) - 1) * 16px); } } } .react-aria-TreeItem { &[data-allows-dragging] { padding-left: 4px; } &[data-dragging] { opacity: 0.6; } &[data-drop-target] { outline: 2px solid var(--highlight-background); background: var(--highlight-overlay); } [slot=drag] { all: unset; width: 15px; text-align: center; &[data-focus-visible] { border-radius: 4px; outline: 2px solid var(--focus-ring-color); } } } .react-aria-Tree { &[data-selection-mode=multiple] { --checkbox-width: 28px; } &[data-allows-dragging] { --drag-button-width: 23px; } .react-aria-DropIndicator { &[data-drop-target] { outline: 1px solid var(--highlight-background); margin-left: calc(8px + var(--checkbox-width, 0px) + var(--drag-button-width, 0px) + 26px + (var(--tree-item-level) - 1) * 16px); } } } .react-aria-TreeItem { &[data-allows-dragging] { padding-left: 4px; } &[data-dragging] { opacity: 0.6; } &[data-drop-target] { outline: 2px solid var(--highlight-background); background: var(--highlight-overlay); } [slot=drag] { all: unset; width: 15px; text-align: center; &[data-focus-visible] { border-radius: 4px; outline: 2px solid var(--focus-ring-color); } } } .react-aria-Tree { &[data-selection-mode=multiple] { --checkbox-width: 28px; } &[data-allows-dragging] { --drag-button-width: 23px; } .react-aria-DropIndicator { &[data-drop-target] { outline: 1px solid var(--highlight-background); margin-left: calc(8px + var(--checkbox-width, 0px) + var(--drag-button-width, 0px) + 26px + (var(--tree-item-level) - 1) * 16px); } } } ### Custom drag preview# By default, the drag preview shown under the user's pointer or finger is a copy of the original element that started the drag. A custom preview can be rendered by implementing the `renderDragPreview` function, passed to `useDragAndDrop`. This receives the dragged data that was returned by `getItems`, and returns a rendered preview for those items. This example renders a custom drag preview which shows the number of items being dragged. function Example() { let tree = useTreeData({ initialItems: items }); let {dragAndDropHooks} = useDragAndDrop({ getItems: (keys) => [...keys].map(key => ({ 'text/plain': tree.getItem(key).value.title })), renderDragPreview(items) { return ( <div className="drag-preview"> {items[0]['text/plain']} <span className="badge">{items.length}</span> </div> ); } }); return ( <Tree aria-label="Tree with custom drag preview" selectionMode="multiple" items={tree.items} dragAndDropHooks={dragAndDropHooks} > {function renderItem(item) { return ( <MyTreeItem title={item.value.title}> <Collection items={item.children}> {renderItem} </Collection> </MyTreeItem> ) }} </Tree> ); } function Example() { let tree = useTreeData({ initialItems: items }); let {dragAndDropHooks} = useDragAndDrop({ getItems: (keys) => [...keys].map(key => ({ 'text/plain': tree.getItem(key).value.title })), renderDragPreview(items) { return ( <div className="drag-preview"> {items[0]['text/plain']} <span className="badge">{items.length}</span> </div> ); } }); return ( <Tree aria-label="Tree with custom drag preview" selectionMode="multiple" items={tree.items} dragAndDropHooks={dragAndDropHooks} > {function renderItem(item) { return ( <MyTreeItem title={item.value.title}> <Collection items={item.children}> {renderItem} </Collection> </MyTreeItem> ) }} </Tree> ); } function Example() { let tree = useTreeData( { initialItems: items } ); let { dragAndDropHooks } = useDragAndDrop({ getItems: (keys) => [...keys].map( (key) => ({ 'text/plain': tree.getItem( key ).value.title }) ), renderDragPreview( items ) { return ( <div className="drag-preview"> {items[0][ 'text/plain' ]} <span className="badge"> {items .length} </span> </div> ); } }); return ( <Tree aria-label="Tree with custom drag preview" selectionMode="multiple" items={tree.items} dragAndDropHooks={dragAndDropHooks} > {function renderItem( item ) { return ( <MyTreeItem title={item .value .title} > <Collection items={item .children} > {renderItem} </Collection> </MyTreeItem> ); }} </Tree> ); } ≡ Documents ≡ Photos Show CSS .drag-preview { padding: 4px 8px; display: flex; align-items: center; justify-content: space-between; gap: 8px; background: var(--highlight-background); color: var(--highlight-foreground); border-radius: 4px; .badge { background: var(--highlight-foreground); color: var(--highlight-background); padding: 0 8px; border-radius: 4px; } } .drag-preview { padding: 4px 8px; display: flex; align-items: center; justify-content: space-between; gap: 8px; background: var(--highlight-background); color: var(--highlight-foreground); border-radius: 4px; .badge { background: var(--highlight-foreground); color: var(--highlight-background); padding: 0 8px; border-radius: 4px; } } .drag-preview { padding: 4px 8px; display: flex; align-items: center; justify-content: space-between; gap: 8px; background: var(--highlight-background); color: var(--highlight-foreground); border-radius: 4px; .badge { background: var(--highlight-foreground); color: var(--highlight-background); padding: 0 8px; border-radius: 4px; } } ### Drag data# Data for draggable items can be provided in multiple formats at once. This allows drop targets to choose data in a format that they understand. For example, you could serialize a complex object as JSON in a custom format for use within your own application, and also provide plain text and/or rich HTML fallbacks that can be used when a user drops data in an external application (e.g. an email message). This can be done by returning multiple keys for an item from the `getItems` function. Types can either be a standard mime type for interoperability with external applications, or a custom string for use within your own app. This example provides representations of each item as plain text, HTML, and a custom app-specific data format. Dropping on the drop targets in this page will use the custom data format to render formatted items. If you drop in an external application supporting rich text, the HTML representation will be used. Dropping in a text editor will use the plain text format. function DraggableTree() { let tree = useTreeData({ initialItems: items }); let {dragAndDropHooks} = useDragAndDrop({ getItems(keys) { return [...keys].map(key => { let item = tree.getItem(key).value; return { 'text/plain': `${item.title}`, 'text/html': `<strong>${item.title}</strong>`, 'custom-app-type': JSON.stringify(item) }; }); }, renderDragPreview(items) { return ( <div className="drag-preview"> {items[0]['text/plain']} <span className="badge">{items.length}</span> </div> ); } }); return ( <Tree aria-label="Draggable tree" selectionMode="multiple" items={tree.items} dragAndDropHooks={dragAndDropHooks} > {function renderItem(item) { return ( <MyTreeItem title={item.value.title}> <Collection items={item.children}> {renderItem} </Collection> </MyTreeItem> ) }} </Tree> ); } <div style={{display: 'flex', gap: 12, flexWrap: 'wrap'}}> <DraggableTree /> {/* see below */} <DroppableTree /> </div> function DraggableTree() { let tree = useTreeData({ initialItems: items }); let {dragAndDropHooks} = useDragAndDrop({ getItems(keys) { return [...keys].map(key => { let item = tree.getItem(key).value; return { 'text/plain': `${item.title}`, 'text/html': `<strong>${item.title}</strong>`, 'custom-app-type': JSON.stringify(item) }; }); }, renderDragPreview(items) { return ( <div className="drag-preview"> {items[0]['text/plain']} <span className="badge">{items.length}</span> </div> ); } }); return ( <Tree aria-label="Draggable tree" selectionMode="multiple" items={tree.items} dragAndDropHooks={dragAndDropHooks} > {function renderItem(item) { return ( <MyTreeItem title={item.value.title}> <Collection items={item.children}> {renderItem} </Collection> </MyTreeItem> ) }} </Tree> ); } <div style={{display: 'flex', gap: 12, flexWrap: 'wrap'}}> <DraggableTree /> {/* see below */} <DroppableTree /> </div> function DraggableTree() { let tree = useTreeData( { initialItems: items } ); let { dragAndDropHooks } = useDragAndDrop({ getItems(keys) { return [...keys] .map((key) => { let item = tree.getItem( key ).value; return { 'text/plain': `${item.title}`, 'text/html': `<strong>${item.title}</strong>`, 'custom-app-type': JSON .stringify( item ) }; }); }, renderDragPreview( items ) { return ( <div className="drag-preview"> {items[0][ 'text/plain' ]} <span className="badge"> {items .length} </span> </div> ); } }); return ( <Tree aria-label="Draggable tree" selectionMode="multiple" items={tree.items} dragAndDropHooks={dragAndDropHooks} > {function renderItem( item ) { return ( <MyTreeItem title={item .value .title} > <Collection items={item .children} > {renderItem} </Collection> </MyTreeItem> ); }} </Tree> ); } <div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }} > <DraggableTree /> {/* see below */} <DroppableTree /> </div> ≡ Documents ≡ Photos Drop items here ### Dropping on the collection# Dropping on the Tree as a whole can be enabled using the `onRootDrop` event. When a valid drag hovers over the Tree, it receives the `isDropTarget` state and can be styled using the `[data-drop-target]` CSS selector. import {isTextDropItem} from 'react-aria-components'; interface DroppableItem { id: string | number; title: string; children?: DroppableItem[]; } function Example() { let [items, setItems] = React.useState<any[]>([]); let {dragAndDropHooks} = useDragAndDrop({ async onRootDrop(e) { let items = await Promise.all( e.items .filter(isTextDropItem) .map(async item => JSON.parse(await item.getText('custom-app-type'))) ); setItems(items); } }); return ( <div style={{display: 'flex', gap: 12, flexWrap: 'wrap'}}> <DraggableTree /> <Tree aria-label="Drop target tree" items={items} dragAndDropHooks={dragAndDropHooks} renderEmptyState={() => 'Drop items here'} > {function renderItem(item: DroppableItem) { return ( <MyTreeItem title={item.title}> <Collection items={item.children}> {renderItem} </Collection> </MyTreeItem> ) }} </Tree> </div> ); } import {isTextDropItem} from 'react-aria-components'; interface DroppableItem { id: string | number; title: string; children?: DroppableItem[]; } function Example() { let [items, setItems] = React.useState<any[]>([]); let { dragAndDropHooks } = useDragAndDrop({ async onRootDrop(e) { let items = await Promise.all( e.items .filter(isTextDropItem) .map(async (item) => JSON.parse( await item.getText('custom-app-type') ) ) ); setItems(items); } }); return ( <div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }} > <DraggableTree /> <Tree aria-label="Drop target tree" items={items} dragAndDropHooks={dragAndDropHooks} renderEmptyState={() => 'Drop items here'} > {function renderItem(item: DroppableItem) { return ( <MyTreeItem title={item.title}> <Collection items={item.children}> {renderItem} </Collection> </MyTreeItem> ); }} </Tree> </div> ); } import {isTextDropItem} from 'react-aria-components'; interface DroppableItem { id: string | number; title: string; children?: DroppableItem[]; } function Example() { let [items, setItems] = React.useState< any[] >([]); let { dragAndDropHooks } = useDragAndDrop({ async onRootDrop(e) { let items = await Promise .all( e.items .filter( isTextDropItem ) .map( async (item) => JSON .parse( await item .getText( 'custom-app-type' ) ) ) ); setItems(items); } }); return ( <div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }} > <DraggableTree /> <Tree aria-label="Drop target tree" items={items} dragAndDropHooks={dragAndDropHooks} renderEmptyState={() => 'Drop items here'} > {function renderItem( item: DroppableItem ) { return ( <MyTreeItem title={item .title} > <Collection items={item .children} > {renderItem} </Collection> </MyTreeItem> ); }} </Tree> </div> ); } ≡ Documents ≡ Photos Drop items here Show CSS .react-aria-Tree[data-drop-target] { outline: 2px solid var(--highlight-background); outline-offset: -1px; background: var(--highlight-overlay); } .react-aria-Tree[data-drop-target] { outline: 2px solid var(--highlight-background); outline-offset: -1px; background: var(--highlight-overlay); } .react-aria-Tree[data-drop-target] { outline: 2px solid var(--highlight-background); outline-offset: -1px; background: var(--highlight-overlay); } ### Dropping on items# Dropping on items can be enabled using the `onItemDrop` event. When a valid drag hovers over an item, it receives the `isDropTarget` state and can be styled using the `[data-drop-target]` CSS selector. const droppableItems = [ { id: '8', title: 'Videos', type: 'directory', children: [ { id: '9', title: 'Movie.mp4', type: 'file' } ] } // ... ]; function DroppableTreeExample() { let tree = useTreeData({ initialItems: droppableItems }); let serializeItem = (nodeItem) => ({ ...nodeItem, children: nodeItem.children.map(serializeItem), // Assign a unique ID to avoid duplicates when the same item is dropped multiple times. id: Math.random().toString(36).slice(2) }); let { dragAndDropHooks } = useDragAndDrop({ async onItemDrop(e) { let items = await Promise.all( e.items .filter(isTextDropItem) .map(async (item) => { let parsed = JSON.parse(await item.getText('custom-app-type')); return serializeItem(parsed); }) ); tree.insert(e.target.key, 0, ...items); } }); return ( <div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }}> <DraggableTree /> <Tree aria-label="Tree with item drop targets" items={tree.items} dragAndDropHooks={dragAndDropHooks} > {function renderItem(item) { return ( <MyTreeItem title={item.value.title}> <Collection items={item.children}> {renderItem} </Collection> </MyTreeItem> ); }} </Tree> </div> ); } const droppableItems = [ { id: '8', title: 'Videos', type: 'directory', children: [ { id: '9', title: 'Movie.mp4', type: 'file' } ] } // ... ]; function DroppableTreeExample() { let tree = useTreeData({ initialItems: droppableItems }); let serializeItem = (nodeItem) => ({ ...nodeItem, children: nodeItem.children.map(serializeItem), // Assign a unique ID to avoid duplicates when the same item is dropped multiple times. id: Math.random().toString(36).slice(2) }); let { dragAndDropHooks } = useDragAndDrop({ async onItemDrop(e) { let items = await Promise.all( e.items .filter(isTextDropItem) .map(async (item) => { let parsed = JSON.parse( await item.getText('custom-app-type') ); return serializeItem(parsed); }) ); tree.insert(e.target.key, 0, ...items); } }); return ( <div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }} > <DraggableTree /> <Tree aria-label="Tree with item drop targets" items={tree.items} dragAndDropHooks={dragAndDropHooks} > {function renderItem(item) { return ( <MyTreeItem title={item.value.title}> <Collection items={item.children}> {renderItem} </Collection> </MyTreeItem> ); }} </Tree> </div> ); } const droppableItems = [ { id: '8', title: 'Videos', type: 'directory', children: [ { id: '9', title: 'Movie.mp4', type: 'file' } ] } // ... ]; function DroppableTreeExample() { let tree = useTreeData( { initialItems: droppableItems } ); let serializeItem = ( nodeItem ) => ({ ...nodeItem, children: nodeItem .children.map( serializeItem ), // Assign a unique ID to avoid duplicates when the same item is dropped multiple times. id: Math.random() .toString(36) .slice(2) }); let { dragAndDropHooks } = useDragAndDrop({ async onItemDrop(e) { let items = await Promise .all( e.items .filter( isTextDropItem ) .map( async (item) => { let parsed = JSON .parse( await item .getText( 'custom-app-type' ) ); return serializeItem( parsed ); } ) ); tree.insert( e.target.key, 0, ...items ); } }); return ( <div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }} > <DraggableTree /> <Tree aria-label="Tree with item drop targets" items={tree .items} dragAndDropHooks={dragAndDropHooks} > {function renderItem( item ) { return ( <MyTreeItem title={item .value .title} > <Collection items={item .children} > {renderItem} </Collection> </MyTreeItem> ); }} </Tree> </div> ); } ≡ Documents ≡ Photos Videos Music ### Dropping between items# Dropping between items can be enabled using the `onInsert` event. Tree renders a `DropIndicator` between items to indicate the insertion position, which can be styled using the `.react-aria-DropIndicator` selector. When it is active, it receives the `[data-drop-target]` state. import {isTextDropItem} from 'react-aria-components'; function Example() { let tree = useTreeData({ initialItems: droppableItems }); let serializeItem = (nodeItem) => ({ ...nodeItem, children: nodeItem.children.map(serializeItem), // Assign a unique ID to avoid duplicates when the same item is dropped multiple times. id: Math.random().toString(36).slice(2) }); let { dragAndDropHooks } = useDragAndDrop({ async onInsert(e) { let items = await Promise.all( e.items .filter(isTextDropItem) .map(async (item) => { let parsed = JSON.parse(await item.getText('custom-app-type')); return serializeItem(parsed); }) ); if (e.target.dropPosition === 'before') { tree.insertBefore(e.target.key, ...items); } else if (e.target.dropPosition === 'after') { tree.insertAfter(e.target.key, ...items); } } }); return ( <div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }}> <DraggableTree /> <Tree aria-label="Tree with insertion" items={tree.items} dragAndDropHooks={dragAndDropHooks} > {function renderItem(item) { return ( <MyTreeItem title={item.value.title}> <Collection items={item.children}> {renderItem} </Collection> </MyTreeItem> ); }} </Tree> </div> ); } import {isTextDropItem} from 'react-aria-components'; function Example() { let tree = useTreeData({ initialItems: droppableItems }); let serializeItem = (nodeItem) => ({ ...nodeItem, children: nodeItem.children.map(serializeItem), // Assign a unique ID to avoid duplicates when the same item is dropped multiple times. id: Math.random().toString(36).slice(2) }); let { dragAndDropHooks } = useDragAndDrop({ async onInsert(e) { let items = await Promise.all( e.items .filter(isTextDropItem) .map(async (item) => { let parsed = JSON.parse( await item.getText('custom-app-type') ); return serializeItem(parsed); }) ); if (e.target.dropPosition === 'before') { tree.insertBefore(e.target.key, ...items); } else if (e.target.dropPosition === 'after') { tree.insertAfter(e.target.key, ...items); } } }); return ( <div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }} > <DraggableTree /> <Tree aria-label="Tree with insertion" items={tree.items} dragAndDropHooks={dragAndDropHooks} > {function renderItem(item) { return ( <MyTreeItem title={item.value.title}> <Collection items={item.children}> {renderItem} </Collection> </MyTreeItem> ); }} </Tree> </div> ); } import {isTextDropItem} from 'react-aria-components'; function Example() { let tree = useTreeData( { initialItems: droppableItems } ); let serializeItem = ( nodeItem ) => ({ ...nodeItem, children: nodeItem .children.map( serializeItem ), // Assign a unique ID to avoid duplicates when the same item is dropped multiple times. id: Math.random() .toString(36) .slice(2) }); let { dragAndDropHooks } = useDragAndDrop({ async onInsert(e) { let items = await Promise .all( e.items .filter( isTextDropItem ) .map( async (item) => { let parsed = JSON .parse( await item .getText( 'custom-app-type' ) ); return serializeItem( parsed ); } ) ); if ( e.target .dropPosition === 'before' ) { tree .insertBefore( e.target.key, ...items ); } else if ( e.target .dropPosition === 'after' ) { tree.insertAfter( e.target.key, ...items ); } } }); return ( <div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }} > <DraggableTree /> <Tree aria-label="Tree with insertion" items={tree .items} dragAndDropHooks={dragAndDropHooks} > {function renderItem( item ) { return ( <MyTreeItem title={item .value .title} > <Collection items={item .children} > {renderItem} </Collection> </MyTreeItem> ); }} </Tree> </div> ); } ≡ Documents ≡ Photos Videos Music ### Drop data# `Tree` allows users to drop one or more **drag items**, each of which contains data to be transferred from the drag source to drop target. There are three kinds of drag items: * `text` – represents data inline as a string in one or more formats * `file` – references a file on the user's device * `directory` – references the contents of a directory #### Text# A `TextDropItem` represents textual data in one or more different formats. These may be either standard mime types or custom app-specific formats. Representing data in multiple formats allows drop targets both within and outside an application to choose data in a format that they understand. For example, a complex object may be serialized in a custom format for use within an application, with fallbacks in plain text and/or rich HTML that can be used when a user drops data from an external application. The example below uses the `acceptedDragTypes` prop to accept items that include a custom app-specific type, which is retrieved using the item's `getText` method. The same draggable component as used in the above example is used here, but rather than displaying the plain text representation, the custom format is used instead. When `acceptedDragTypes` is specified, the dropped items are filtered to include only items that include the accepted types. import {isTextDropItem} from 'react-aria-components'; interface DroppableItem { id: string | number; title: string; children?: DroppableItem[]; } function DroppableTree() { let [items, setItems] = React.useState<any[]>([]); let serializeItem = (nodeItem) => ({ ...nodeItem, children: nodeItem.children.map(serializeItem), // Assign a unique ID to avoid duplicates when the same item is dropped multiple times. id: Math.random().toString(36).slice(2) }); let { dragAndDropHooks } = useDragAndDrop({ acceptedDragTypes: ['custom-app-type'], async onRootDrop(e) { let items = await Promise.all( e.items .filter(isTextDropItem) .map(async (item) => { let parsed = JSON.parse(await item.getText('custom-app-type')); return serializeItem(parsed); }) ); setItems(items); } }); return ( <Tree aria-label="Droppable tree" items={items} dragAndDropHooks={dragAndDropHooks} renderEmptyState={() => 'Drop items here'} > {function renderItem(item: DroppableItem) { return ( <MyTreeItem title={item.title}> <Collection items={item.children}> {renderItem} </Collection> </MyTreeItem> ); }} </Tree> ); } <div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }}> <DraggableTree /> <DroppableTree /> </div> import {isTextDropItem} from 'react-aria-components'; interface DroppableItem { id: string | number; title: string; children?: DroppableItem[]; } function DroppableTree() { let [items, setItems] = React.useState<any[]>([]); let serializeItem = (nodeItem) => ({ ...nodeItem, children: nodeItem.children.map(serializeItem), // Assign a unique ID to avoid duplicates when the same item is dropped multiple times. id: Math.random().toString(36).slice(2) }); let { dragAndDropHooks } = useDragAndDrop({ acceptedDragTypes: ['custom-app-type'], async onRootDrop(e) { let items = await Promise.all( e.items .filter(isTextDropItem) .map(async (item) => { let parsed = JSON.parse( await item.getText('custom-app-type') ); return serializeItem(parsed); }) ); setItems(items); } }); return ( <Tree aria-label="Droppable tree" items={items} dragAndDropHooks={dragAndDropHooks} renderEmptyState={() => 'Drop items here'} > {function renderItem(item: DroppableItem) { return ( <MyTreeItem title={item.title}> <Collection items={item.children}> {renderItem} </Collection> </MyTreeItem> ); }} </Tree> ); } <div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }} > <DraggableTree /> <DroppableTree /> </div> import {isTextDropItem} from 'react-aria-components'; interface DroppableItem { id: string | number; title: string; children?: DroppableItem[]; } function DroppableTree() { let [items, setItems] = React.useState< any[] >([]); let serializeItem = ( nodeItem ) => ({ ...nodeItem, children: nodeItem .children.map( serializeItem ), // Assign a unique ID to avoid duplicates when the same item is dropped multiple times. id: Math.random() .toString(36) .slice(2) }); let { dragAndDropHooks } = useDragAndDrop({ acceptedDragTypes: [ 'custom-app-type' ], async onRootDrop(e) { let items = await Promise .all( e.items .filter( isTextDropItem ) .map( async (item) => { let parsed = JSON .parse( await item .getText( 'custom-app-type' ) ); return serializeItem( parsed ); } ) ); setItems(items); } }); return ( <Tree aria-label="Droppable tree" items={items} dragAndDropHooks={dragAndDropHooks} renderEmptyState={() => 'Drop items here'} > {function renderItem( item: DroppableItem ) { return ( <MyTreeItem title={item .title} > <Collection items={item .children} > {renderItem} </Collection> </MyTreeItem> ); }} </Tree> ); } <div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }} > <DraggableTree /> <DroppableTree /> </div> ≡ Documents ≡ Photos Drop items here #### Files# A `FileDropItem` references a file on the user's device. It includes the name and mime type of the file, and methods to read the contents as plain text, or retrieve a native File object which can be attached to form data for uploading. This example accepts JPEG and PNG image files, and renders them in a tree structure by creating a local object URL. When the tree is empty, you can drop on the whole collection, and otherwise items can be inserted. import {isFileDropItem} from 'react-aria-components'; interface ImageItem { id: number; url: string; name: string; type: string; lastModified: number; } function Example() { let [items, setItems] = React.useState<ImageItem[]>([]); let { dragAndDropHooks } = useDragAndDrop({ acceptedDragTypes: ['image/jpeg', 'image/png'], async onRootDrop(e) { let items = await Promise.all( e.items.filter(isFileDropItem).map(async (item) => { let file = await item.getFile(); return { id: Math.random(), url: URL.createObjectURL(file), name: item.name, type: file.type, lastModified: file.lastModified }; }) ); setItems(items); } }); return ( <Tree aria-label="Droppable tree" items={items} dragAndDropHooks={dragAndDropHooks} renderEmptyState={() => 'Drop images here'} > {(item) => ( <TreeItem textValue={item.name}> <TreeItemContent> <img src={item.url} style={{ width: 20, height: 20, marginRight: 8 }} /> {item.name} </TreeItemContent> </TreeItem> )} </Tree> ); } import {isFileDropItem} from 'react-aria-components'; interface ImageItem { id: number; url: string; name: string; type: string; lastModified: number; } function Example() { let [items, setItems] = React.useState<ImageItem[]>([]); let { dragAndDropHooks } = useDragAndDrop({ acceptedDragTypes: ['image/jpeg', 'image/png'], async onRootDrop(e) { let items = await Promise.all( e.items.filter(isFileDropItem).map(async (item) => { let file = await item.getFile(); return { id: Math.random(), url: URL.createObjectURL(file), name: item.name, type: file.type, lastModified: file.lastModified }; }) ); setItems(items); } }); return ( <Tree aria-label="Droppable tree" items={items} dragAndDropHooks={dragAndDropHooks} renderEmptyState={() => 'Drop images here'} > {(item) => ( <TreeItem textValue={item.name}> <TreeItemContent> <img src={item.url} style={{ width: 20, height: 20, marginRight: 8 }} /> {item.name} </TreeItemContent> </TreeItem> )} </Tree> ); } import {isFileDropItem} from 'react-aria-components'; interface ImageItem { id: number; url: string; name: string; type: string; lastModified: number; } function Example() { let [items, setItems] = React.useState< ImageItem[] >([]); let { dragAndDropHooks } = useDragAndDrop({ acceptedDragTypes: [ 'image/jpeg', 'image/png' ], async onRootDrop(e) { let items = await Promise .all( e.items .filter( isFileDropItem ).map( async (item) => { let file = await item .getFile(); return { id: Math .random(), url: URL .createObjectURL( file ), name: item .name, type: file .type, lastModified: file .lastModified }; } ) ); setItems(items); } }); return ( <Tree aria-label="Droppable tree" items={items} dragAndDropHooks={dragAndDropHooks} renderEmptyState={() => 'Drop images here'} > {(item) => ( <TreeItem textValue={item .name} > <TreeItemContent> <img src={item .url} style={{ width: 20, height: 20, marginRight: 8 }} /> {item.name} </TreeItemContent> </TreeItem> )} </Tree> ); } Drop images here #### Directories# A `DirectoryDropItem` references the contents of a directory on the user's device. It includes the name of the directory, as well as a method to iterate through the files and folders within the directory. The contents of any folders within the directory can be accessed recursively. The `getEntries` method returns an async iterable object, which can be used in a `for await...of` loop. This provides each item in the directory as either a `FileDropItem` or` DirectoryDropItem `, and you can access the contents of each file as discussed above. This example accepts directory drops over the whole collection, and renders the contents as items in the tree. `DIRECTORY_DRAG_TYPE` is imported from `react-aria-components` and included in the `acceptedDragTypes` prop to limit the accepted items to only directories. import {DIRECTORY_DRAG_TYPE, isDirectoryDropItem} from 'react-aria-components'; interface DirItem { name: string; kind: string; type: string; children?: DirItem[]; } function Example() { let [files, setFiles] = React.useState<DirItem[]>([]); let { dragAndDropHooks } = useDragAndDrop({ acceptedDragTypes: [DIRECTORY_DRAG_TYPE], async onRootDrop(e) { // Read entries in directory and update state with relevant info. let getFiles = async (dir) => { let files = []; for await (let entry of dir.getEntries()) { files.push({ id: Math.random().toString(36).slice(2), name: entry.name, kind: entry.kind, children: entry.kind === 'directory' ? await getFiles(entry) : [] }); } return files; }; let dir = e.items.find(isDirectoryDropItem)!; setFiles(await getFiles(dir)); } }); return ( <Tree aria-label="Droppable tree" items={files} dragAndDropHooks={dragAndDropHooks} renderEmptyState={() => 'Drop directory here'} > {function renderItem(item) { return ( <MyTreeItem title={`${item.kind === 'directory' ? '📁' : '📄'} ${item.name}`} > <Collection items={item.children}> {renderItem} </Collection> </MyTreeItem> ); }} </Tree> ); } import { DIRECTORY_DRAG_TYPE, isDirectoryDropItem } from 'react-aria-components'; interface DirItem { name: string; kind: string; type: string; children?: DirItem[]; } function Example() { let [files, setFiles] = React.useState<DirItem[]>([]); let { dragAndDropHooks } = useDragAndDrop({ acceptedDragTypes: [DIRECTORY_DRAG_TYPE], async onRootDrop(e) { // Read entries in directory and update state with relevant info. let getFiles = async (dir) => { let files = []; for await (let entry of dir.getEntries()) { files.push({ id: Math.random().toString(36).slice(2), name: entry.name, kind: entry.kind, children: entry.kind === 'directory' ? await getFiles(entry) : [] }); } return files; }; let dir = e.items.find(isDirectoryDropItem)!; setFiles(await getFiles(dir)); } }); return ( <Tree aria-label="Droppable tree" items={files} dragAndDropHooks={dragAndDropHooks} renderEmptyState={() => 'Drop directory here'} > {function renderItem(item) { return ( <MyTreeItem title={`${ item.kind === 'directory' ? '📁' : '📄' } ${item.name}`} > <Collection items={item.children}> {renderItem} </Collection> </MyTreeItem> ); }} </Tree> ); } import { DIRECTORY_DRAG_TYPE, isDirectoryDropItem } from 'react-aria-components'; interface DirItem { name: string; kind: string; type: string; children?: DirItem[]; } function Example() { let [files, setFiles] = React.useState< DirItem[] >([]); let { dragAndDropHooks } = useDragAndDrop({ acceptedDragTypes: [ DIRECTORY_DRAG_TYPE ], async onRootDrop(e) { // Read entries in directory and update state with relevant info. let getFiles = async (dir) => { let files = []; for await ( let entry of dir .getEntries() ) { files.push({ id: Math .random() .toString( 36 ).slice( 2 ), name: entry .name, kind: entry .kind, children: entry .kind === 'directory' ? await getFiles( entry ) : [] }); } return files; }; let dir = e.items .find( isDirectoryDropItem )!; setFiles( await getFiles( dir ) ); } }); return ( <Tree aria-label="Droppable tree" items={files} dragAndDropHooks={dragAndDropHooks} renderEmptyState={() => 'Drop directory here'} > {function renderItem( item ) { return ( <MyTreeItem title={`${ item .kind === 'directory' ? '📁' : '📄' } ${item.name}`} > <Collection items={item .children} > {renderItem} </Collection> </MyTreeItem> ); }} </Tree> ); } Drop directory here ### Drop operations# A `DropOperation` is an indication of what will happen when dragged data is dropped on a particular drop target. These are: * `move` – indicates that the dragged data will be moved from its source location to the target location. * `copy` – indicates that the dragged data will be copied to the target destination. * `link` – indicates that there will be a relationship established between the source and target locations. * `cancel` – indicates that the drag and drop operation will be canceled, resulting in no changes made to the source or target. Many operating systems display these in the form of a cursor change, e.g. a plus sign to indicate a copy operation. The user may also be able to use a modifier key to choose which drop operation to perform, such as Option or Alt to switch from move to copy. #### onDragEnd# The `onDragEnd` event allows the drag source to respond when a drag that it initiated ends, either because it was dropped or because it was canceled by the user. The `dropOperation` property of the event object indicates the operation that was performed. For example, when data is moved, the UI could be updated to reflect this change by removing the original dragged items. This example removes the dragged items from the UI when a move operation is completed. Try holding the Option or Alt keys to change the operation to copy, and see how the behavior changes. function Example() { let tree = useTreeData({ initialItems: items }); let { dragAndDropHooks } = useDragAndDrop({ // ... onDragEnd(e) { if (e.dropOperation === 'move') { tree.remove(...e.keys); } } }); return ( <div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }}> <Tree aria-label="Draggable tree" selectionMode="multiple" items={tree.items} dragAndDropHooks={dragAndDropHooks} > {function renderItem(item) { return ( <MyTreeItem title={item.value.title}> <Collection items={item.children}> {renderItem} </Collection> </MyTreeItem> ); }} </Tree> <DroppableTree /> </div> ); } function Example() { let tree = useTreeData({ initialItems: items }); let { dragAndDropHooks } = useDragAndDrop({ // ... onDragEnd(e) { if (e.dropOperation === 'move') { tree.remove(...e.keys); } } }); return ( <div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }} > <Tree aria-label="Draggable tree" selectionMode="multiple" items={tree.items} dragAndDropHooks={dragAndDropHooks} > {function renderItem(item) { return ( <MyTreeItem title={item.value.title}> <Collection items={item.children}> {renderItem} </Collection> </MyTreeItem> ); }} </Tree> <DroppableTree /> </div> ); } function Example() { let tree = useTreeData( { initialItems: items } ); let { dragAndDropHooks } = useDragAndDrop({ // ... onDragEnd(e) { if ( e.dropOperation === 'move' ) { tree.remove( ...e.keys ); } } }); return ( <div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }} > <Tree aria-label="Draggable tree" selectionMode="multiple" items={tree .items} dragAndDropHooks={dragAndDropHooks} > {function renderItem( item ) { return ( <MyTreeItem title={item .value .title} > <Collection items={item .children} > {renderItem} </Collection> </MyTreeItem> ); }} </Tree> <DroppableTree /> </div> ); } ≡ Documents ≡ Photos Drop items here #### getAllowedDropOperations# The drag source can also control which drop operations are allowed for the data. For example, if moving data is not allowed, and only copying is supported, the `getAllowedDropOperations` function could be implemented to indicate this. When you drag the element below, the cursor now shows the copy affordance by default, and pressing a modifier to switch drop operations results in the drop being canceled. function Example() { // ... let { dragAndDropHooks } = useDragAndDrop({ // ... getAllowedDropOperations: () => ['copy'] }); return ( <div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }}> <Tree aria-label="Draggable tree" selectionMode="multiple" items={tree.items} dragAndDropHooks={dragAndDropHooks} > {function renderItem(item) { return ( <MyTreeItem title={item.value.title}> <Collection items={item.children}> {renderItem} </Collection> </MyTreeItem> ); }} </Tree> <DroppableTree /> </div> ); } function Example() { // ... let { dragAndDropHooks } = useDragAndDrop({ // ... getAllowedDropOperations: () => ['copy'] }); return ( <div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }} > <Tree aria-label="Draggable tree" selectionMode="multiple" items={tree.items} dragAndDropHooks={dragAndDropHooks} > {function renderItem(item) { return ( <MyTreeItem title={item.value.title}> <Collection items={item.children}> {renderItem} </Collection> </MyTreeItem> ); }} </Tree> <DroppableTree /> </div> ); } function Example() { // ... let { dragAndDropHooks } = useDragAndDrop({ // ... getAllowedDropOperations: () => ['copy'] }); return ( <div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }} > <Tree aria-label="Draggable tree" selectionMode="multiple" items={tree .items} dragAndDropHooks={dragAndDropHooks} > {function renderItem( item ) { return ( <MyTreeItem title={item .value .title} > <Collection items={item .children} > {renderItem} </Collection> </MyTreeItem> ); }} </Tree> <DroppableTree /> </div> ); } ≡ Documents ≡ Photos Drop items here #### getDropOperation# The `getDropOperation` function passed to `useDragAndDrop` can be used to provide appropriate feedback to the user when a drag hovers over the drop target. This function receives the drop target, set of types contained in the drag, and a list of allowed drop operations as specified by the drag source. It should return one of the drop operations in `allowedOperations`, or a specific drop operation if only that drop operation is supported. It may also return `'cancel'` to reject the drop. If the returned operation is not in `allowedOperations`, then the drop target will act as if `'cancel'` was returned. In the below example, the drop target only supports dropping PNG images. If a PNG is dragged over the target, it will be highlighted and the operating system displays a copy cursor. If another type is dragged over the target, then there is no visual feedback, indicating that a drop is not accepted there. If the user holds a modifier key such as Control while dragging over the drop target in order to change the drop operation, then the drop target does not accept the drop. function Example() { let [items, setItems] = React.useState<ImageItem[]>([]); let { dragAndDropHooks } = useDragAndDrop({ getDropOperation: () => 'copy', acceptedDragTypes: ['image/png'], async onRootDrop(e) { // ... } }); // See "Files" example above... } function Example() { let [items, setItems] = React.useState<ImageItem[]>([]); let { dragAndDropHooks } = useDragAndDrop({ getDropOperation: () => 'copy', acceptedDragTypes: ['image/png'], async onRootDrop(e) { // ... } }); // See "Files" example above... } function Example() { let [items, setItems] = React.useState< ImageItem[] >([]); let { dragAndDropHooks } = useDragAndDrop({ getDropOperation: () => 'copy', acceptedDragTypes: [ 'image/png' ], async onRootDrop(e) { // ... } }); // See "Files" example above... } Drop PNGs here #### Drop events# Drop events such as `onInsert`, `onItemDrop`, etc. also include the `dropOperation`. This can be used to perform different actions accordingly, for example, when communicating with a backend API. let onItemDrop = async (e) => { let data = JSON.parse(await e.items[0].getText('my-app-file')); switch (e.dropOperation) { case 'move': MyAppFileService.move(data.filePath, props.filePath); break; case 'copy': MyAppFileService.copy(data.filePath, props.filePath); break; case 'link': MyAppFileService.link(data.filePath, props.filePath); break; }}; let onItemDrop = async (e) => { let data = JSON.parse( await e.items[0].getText('my-app-file') ); switch (e.dropOperation) { case 'move': MyAppFileService.move(data.filePath, props.filePath); break; case 'copy': MyAppFileService.copy(data.filePath, props.filePath); break; case 'link': MyAppFileService.link(data.filePath, props.filePath); break; }}; let onItemDrop = async ( e ) => { let data = JSON.parse( await e.items[0] .getText( 'my-app-file' ) ); switch ( e.dropOperation ) { case 'move': MyAppFileService .move( data.filePath, props.filePath ); break; case 'copy': MyAppFileService .copy( data.filePath, props.filePath ); break; case 'link': MyAppFileService .link( data.filePath, props.filePath ); break; }}; ### Drag between trees# This example puts together many of the concepts described above, allowing users to drag items between trees bidirectionally. It also supports moving items within the same tree hierarchy. When a tree is empty, it accepts drops on the whole collection. `getDropOperation` ensures that items are always moved rather than copied, which avoids duplicate items. import {isTextDropItem} from 'react-aria-components'; interface FileItem { id: string; title: string; type: string; children?: FileItem[]; } interface DndTreeProps { initialItems: FileItem[]; 'aria-label': string; } function DndTree(props: DndTreeProps) { let tree = useTreeData({ initialItems: props.initialItems, getKey: (item) => item.id, getChildren: (item) => item.children || [] }); let { dragAndDropHooks } = useDragAndDrop({ // Provide drag data in a custom format as well as plain text. getItems(keys) { return [...keys].map((key) => { let item = tree.getItem(key); let serializeItem = (nodeItem) => ({ ...nodeItem.value, children: nodeItem.children ? [...nodeItem.children].map(serializeItem) : [] }); return { 'custom-app-type': JSON.stringify(serializeItem(item)), 'text/plain': item.value.title }; }); }, // Accept drops with the custom format. acceptedDragTypes: ['custom-app-type'], // Ensure items are always moved rather than copied. getDropOperation: () => 'move', // Handle drops between items from other trees. async onInsert(e) { let processedItems = await Promise.all( e.items .filter(isTextDropItem) .map(async (item) => JSON.parse(await item.getText('custom-app-type')) ) ); if (e.target.dropPosition === 'before') { tree.insertBefore(e.target.key, ...processedItems); } else if (e.target.dropPosition === 'after') { tree.insertAfter(e.target.key, ...processedItems); } }, // Handle drops on the collection when empty. async onRootDrop(e) { let processedItems = await Promise.all( e.items .filter(isTextDropItem) .map(async (item) => JSON.parse(await item.getText('custom-app-type')) ) ); tree.insert(null, 0, ...processedItems); }, // Handle moving items within the same tree or to different levels. onMove(e) { if (e.target.dropPosition === 'before') { tree.moveBefore(e.target.key, e.keys); } else if (e.target.dropPosition === 'after') { tree.moveAfter(e.target.key, e.keys); } else if (e.target.dropPosition === 'on') { let targetNode = tree.getItem(e.target.key); if (targetNode) { let targetIndex = targetNode.children ? targetNode.children.length : 0; let keyArray = Array.from(e.keys); for (let i = 0; i < keyArray.length; i++) { tree.move(keyArray[i], e.target.key, targetIndex + i); } } } }, // Remove the items from the source tree on drop // if they were moved to a different tree. onDragEnd(e) { if (e.dropOperation === 'move' && !e.isInternal) { tree.remove(...e.keys); } } }); return ( <Tree aria-label={props['aria-label']} selectionMode="multiple" items={tree.items} dragAndDropHooks={dragAndDropHooks} renderEmptyState={() => 'Drop items here'} > {function renderItem(item) { return ( <MyTreeItem title={item.value.title}> <Collection items={item.children}> {renderItem} </Collection> </MyTreeItem> ); }} </Tree> ); } <div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }}> <DndTree initialItems={items} aria-label="First Tree" /> <DndTree initialItems={droppableItems} aria-label="Second Tree" /> </div> import {isTextDropItem} from 'react-aria-components'; interface FileItem { id: string; title: string; type: string; children?: FileItem[]; } interface DndTreeProps { initialItems: FileItem[]; 'aria-label': string; } function DndTree(props: DndTreeProps) { let tree = useTreeData({ initialItems: props.initialItems, getKey: (item) => item.id, getChildren: (item) => item.children || [] }); let { dragAndDropHooks } = useDragAndDrop({ // Provide drag data in a custom format as well as plain text. getItems(keys) { return [...keys].map((key) => { let item = tree.getItem(key); let serializeItem = (nodeItem) => ({ ...nodeItem.value, children: nodeItem.children ? [...nodeItem.children].map(serializeItem) : [] }); return { 'custom-app-type': JSON.stringify( serializeItem(item) ), 'text/plain': item.value.title }; }); }, // Accept drops with the custom format. acceptedDragTypes: ['custom-app-type'], // Ensure items are always moved rather than copied. getDropOperation: () => 'move', // Handle drops between items from other trees. async onInsert(e) { let processedItems = await Promise.all( e.items .filter(isTextDropItem) .map(async (item) => JSON.parse( await item.getText('custom-app-type') ) ) ); if (e.target.dropPosition === 'before') { tree.insertBefore(e.target.key, ...processedItems); } else if (e.target.dropPosition === 'after') { tree.insertAfter(e.target.key, ...processedItems); } }, // Handle drops on the collection when empty. async onRootDrop(e) { let processedItems = await Promise.all( e.items .filter(isTextDropItem) .map(async (item) => JSON.parse( await item.getText('custom-app-type') ) ) ); tree.insert(null, 0, ...processedItems); }, // Handle moving items within the same tree or to different levels. onMove(e) { if (e.target.dropPosition === 'before') { tree.moveBefore(e.target.key, e.keys); } else if (e.target.dropPosition === 'after') { tree.moveAfter(e.target.key, e.keys); } else if (e.target.dropPosition === 'on') { let targetNode = tree.getItem(e.target.key); if (targetNode) { let targetIndex = targetNode.children ? targetNode.children.length : 0; let keyArray = Array.from(e.keys); for (let i = 0; i < keyArray.length; i++) { tree.move( keyArray[i], e.target.key, targetIndex + i ); } } } }, // Remove the items from the source tree on drop // if they were moved to a different tree. onDragEnd(e) { if (e.dropOperation === 'move' && !e.isInternal) { tree.remove(...e.keys); } } }); return ( <Tree aria-label={props['aria-label']} selectionMode="multiple" items={tree.items} dragAndDropHooks={dragAndDropHooks} renderEmptyState={() => 'Drop items here'} > {function renderItem(item) { return ( <MyTreeItem title={item.value.title}> <Collection items={item.children}> {renderItem} </Collection> </MyTreeItem> ); }} </Tree> ); } <div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }} > <DndTree initialItems={items} aria-label="First Tree" /> <DndTree initialItems={droppableItems} aria-label="Second Tree" /> </div> import {isTextDropItem} from 'react-aria-components'; interface FileItem { id: string; title: string; type: string; children?: FileItem[]; } interface DndTreeProps { initialItems: FileItem[]; 'aria-label': string; } function DndTree( props: DndTreeProps ) { let tree = useTreeData( { initialItems: props .initialItems, getKey: (item) => item.id, getChildren: (item) => item .children || [] } ); let { dragAndDropHooks } = useDragAndDrop({ // Provide drag data in a custom format as well as plain text. getItems(keys) { return [...keys] .map((key) => { let item = tree .getItem( key ); let serializeItem = ( nodeItem ) => ({ ...nodeItem .value, children: nodeItem .children ? [ ...nodeItem .children ].map( serializeItem ) : [] }); return { 'custom-app-type': JSON .stringify( serializeItem( item ) ), 'text/plain': item.value .title }; }); }, // Accept drops with the custom format. acceptedDragTypes: [ 'custom-app-type' ], // Ensure items are always moved rather than copied. getDropOperation: () => 'move', // Handle drops between items from other trees. async onInsert(e) { let processedItems = await Promise .all( e.items .filter( isTextDropItem ) .map( async (item) => JSON .parse( await item .getText( 'custom-app-type' ) ) ) ); if ( e.target .dropPosition === 'before' ) { tree .insertBefore( e.target.key, ...processedItems ); } else if ( e.target .dropPosition === 'after' ) { tree.insertAfter( e.target.key, ...processedItems ); } }, // Handle drops on the collection when empty. async onRootDrop(e) { let processedItems = await Promise .all( e.items .filter( isTextDropItem ) .map( async (item) => JSON .parse( await item .getText( 'custom-app-type' ) ) ) ); tree.insert( null, 0, ...processedItems ); }, // Handle moving items within the same tree or to different levels. onMove(e) { if ( e.target .dropPosition === 'before' ) { tree.moveBefore( e.target.key, e.keys ); } else if ( e.target .dropPosition === 'after' ) { tree.moveAfter( e.target.key, e.keys ); } else if ( e.target .dropPosition === 'on' ) { let targetNode = tree.getItem( e.target.key ); if (targetNode) { let targetIndex = targetNode .children ? targetNode .children .length : 0; let keyArray = Array.from( e.keys ); for ( let i = 0; i < keyArray .length; i++ ) { tree.move( keyArray[ i ], e.target .key, targetIndex + i ); } } } }, // Remove the items from the source tree on drop // if they were moved to a different tree. onDragEnd(e) { if ( e.dropOperation === 'move' && !e.isInternal ) { tree.remove( ...e.keys ); } } }); return ( <Tree aria-label={props[ 'aria-label' ]} selectionMode="multiple" items={tree.items} dragAndDropHooks={dragAndDropHooks} renderEmptyState={() => 'Drop items here'} > {function renderItem( item ) { return ( <MyTreeItem title={item .value .title} > <Collection items={item .children} > {renderItem} </Collection> </MyTreeItem> ); }} </Tree> ); } <div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }} > <DndTree initialItems={items} aria-label="First Tree" /> <DndTree initialItems={droppableItems} aria-label="Second Tree" /> </div> ≡ Documents ≡ Photos ≡ Videos ≡ Music ## Props# * * * ### Tree# | Name | Type | Default | Description | | --- | --- | --- | --- | | `selectionBehavior` | ` SelectionBehavior ` | — | How multiple selection should behave in the tree. | | `renderEmptyState` | `( (props: TreeEmptyStateRenderProps )) => ReactNode` | — | Provides content to display when there are no items in the list. | | `disabledBehavior` | ` DisabledBehavior ` | `'all'` | Whether `disabledKeys` applies to all interactions, or only selection. | | `dragAndDropHooks` | ` DragAndDropHooks ` | — | The drag and drop hooks returned by `useDragAndDrop` used to enable drag and drop behavior for the Tree. | | `escapeKeyBehavior` | `'clearSelection' | 'none'` | `'clearSelection'` | Whether pressing the escape key should clear selection in the grid list or not. Most experiences should not modify this option as it eliminates a keyboard user's ability to easily clear selection. Only use if the escape key is being handled externally or should not trigger selection clearing contextually. | | `autoFocus` | `boolean | FocusStrategy ` | — | Whether to auto focus the gridlist or an option. | | `shouldSelectOnPressUp` | `boolean` | — | Whether selection should occur on press up instead of press down. | | `items` | `Iterable<T>` | — | Item objects in the collection. | | `disabledKeys` | `Iterable<Key>` | — | The item keys that are disabled. These items cannot be selected, focused, or otherwise interacted with. | | `selectionMode` | ` SelectionMode ` | — | The type of selection that is allowed in the collection. | | `disallowEmptySelection` | `boolean` | — | Whether the collection allows empty selection. | | `selectedKeys` | `'all' | Iterable<Key>` | — | The currently selected keys in the collection (controlled). | | `defaultSelectedKeys` | `'all' | Iterable<Key>` | — | The initial selected keys in the collection (uncontrolled). | | `children` | `ReactNode | ( (item: object )) => ReactNode` | — | The contents of the collection. | | `dependencies` | `ReadonlyArray<any>` | — | Values that should invalidate the item cache when using dynamic collections. | | `className` | `string | ( (values: TreeRenderProps & & { defaultClassName: string | | undefined } )) => string` | — | The CSS className for the element. A function may be provided to compute the class based on component state. | | `style` | `CSSProperties | ( (values: TreeRenderProps & & { defaultStyle: CSSProperties } )) => CSSProperties | undefined` | — | The inline style for the element. A function may be provided to compute the style based on component state. | | `expandedKeys` | `Iterable<Key>` | — | The currently expanded keys in the collection (controlled). | | `defaultExpandedKeys` | `Iterable<Key>` | — | The initial expanded keys in the collection (uncontrolled). | Events | Name | Type | Description | | --- | --- | --- | | `onAction` | `( (key: Key )) => void` | Handler that is called when a user performs an action on an item. The exact user event depends on the collection's `selectionBehavior` prop and the interaction modality. | | `onSelectionChange` | `( (keys: Selection )) => void` | Handler that is called when the selection changes. | | `onScroll` | `( (e: UIEvent<Element> )) => void` | Handler that is called when a user scrolls. See MDN. | | `onExpandedChange` | `( (keys: Set<Key> )) => any` | Handler that is called when items are expanded or collapsed. | Layout | Name | Type | Description | | --- | --- | --- | | `slot` | `string | null` | A slot name for the component. Slots allow the component to receive props from a parent component. An explicit `null` value indicates that the local props completely override all props received from a parent. | Accessibility | Name | Type | Description | | --- | --- | --- | | `id` | `string` | The element's unique identifier. See MDN. | | `aria-label` | `string` | Defines a string value that labels the current element. | | `aria-labelledby` | `string` | Identifies the element (or elements) that labels the current element. | | `aria-describedby` | `string` | Identifies the element (or elements) that describes the object. | | `aria-details` | `string` | Identifies the element (or elements) that provide a detailed, extended description for the object. | ### TreeItem# | Name | Type | Description | | --- | --- | --- | | `textValue` | `string` | A string representation of the tree item's contents, used for features like typeahead. | | `children` | `ReactNode` | The content of the tree item along with any nested children. Supports static nested tree items or use of a Collection to dynamically render nested tree items. | | `id` | `Key` | The unique id of the tree row. | | `value` | `object` | The object value that this tree item represents. When using dynamic collections, this is set automatically. | | `isDisabled` | `boolean` | Whether the item is disabled. | | `className` | `string | ( (values: TreeItemRenderProps & & { defaultClassName: string | | undefined } )) => string` | The CSS className for the element. A function may be provided to compute the class based on component state. | | `style` | `CSSProperties | ( (values: TreeItemRenderProps & & { defaultStyle: CSSProperties } )) => CSSProperties | undefined` | The inline style for the element. A function may be provided to compute the style based on component state. | | `href` | ` Href ` | A URL to link to. See MDN. | | `hrefLang` | `string` | Hints at the human language of the linked URL. SeeMDN. | | `target` | `HTMLAttributeAnchorTarget` | The target window for the link. See MDN. | | `rel` | `string` | The relationship between the linked resource and the current page. See MDN. | | `download` | `boolean | string` | Causes the browser to download the linked URL. A string may be provided to suggest a file name. See MDN. | | `ping` | `string` | A space-separated list of URLs to ping when the link is followed. See MDN. | | `referrerPolicy` | `HTMLAttributeReferrerPolicy` | How much of the referrer to send when following the link. See MDN. | | `routerOptions` | ` RouterOptions ` | Options for the configured client side router. | | `hasChildItems` | `boolean` | Whether this item has children, even if not loaded yet. | Events | Name | Type | Description | | --- | --- | --- | | `onAction` | `() => void` | Handler that is called when a user performs an action on this tree item. The exact user event depends on the collection's `selectionBehavior` prop and the interaction modality. | | `onHoverStart` | `( (e: HoverEvent )) => void` | Handler that is called when a hover interaction starts. | | `onHoverEnd` | `( (e: HoverEvent )) => void` | Handler that is called when a hover interaction ends. | | `onHoverChange` | `( (isHovering: boolean )) => void` | Handler that is called when the hover state changes. | Accessibility | Name | Type | Description | | --- | --- | --- | | `aria-label` | `string` | An accessibility label for this tree item. | ### TreeItemContent# | Name | Type | Description | | --- | --- | --- | | `children` | `ReactNode | ( (values: T & & { defaultChildren: ReactNode | | undefined } )) => ReactNode` | The children of the component. A function may be provided to alter the children based on component state. | ## Styling# * * * React Aria components can be styled in many ways, including using CSS classes, inline styles, utility classes (e.g. Tailwind), CSS-in-JS (e.g. Styled Components), etc. By default, all components include a builtin `className` attribute which can be targeted using CSS selectors. These follow the `react-aria-ComponentName` naming convention. .react-aria-Tree { /* ... */ } .react-aria-Tree { /* ... */ } .react-aria-Tree { /* ... */ } A custom `className` can also be specified on any component. This overrides the default `className` provided by React Aria with your own. <TreeItem className="my-tree-item"> {/* ... */} </TreeItem> <TreeItem className="my-tree-item"> {/* ... */} </TreeItem> <TreeItem className="my-tree-item"> {/* ... */} </TreeItem> In addition, some components support multiple UI states (e.g. focused, placeholder, readonly, etc.). React Aria components expose states using data attributes, which you can target in CSS selectors. For example: .react-aria-TreeItem[data-expanded] { /* ... */ } .react-aria-TreeItem[data-selected] { /* ... */ } .react-aria-TreeItem[data-expanded] { /* ... */ } .react-aria-TreeItem[data-selected] { /* ... */ } .react-aria-TreeItem[data-expanded] { /* ... */ } .react-aria-TreeItem[data-selected] { /* ... */ } The `className` and `style` props also accept functions which receive states for styling. This lets you dynamically determine the classes or styles to apply, which is useful when using utility CSS libraries like Tailwind. <TreeItem className={({ isSelected }) => isSelected ? 'bg-blue-400' : 'bg-gray-100'} /> <TreeItem className={({ isSelected }) => isSelected ? 'bg-blue-400' : 'bg-gray-100'} /> <TreeItem className={( { isSelected } ) => isSelected ? 'bg-blue-400' : 'bg-gray-100'} /> 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 checkbox only when selection is enabled. <TreeItem> <TreeItemContent> {({selectionMode}) => ( <> {selectionMode !== 'none' && <Checkbox />} Item </> )} </TreeItemContent> </TreeItem> <TreeItem> <TreeItemContent> {({selectionMode}) => ( <> {selectionMode !== 'none' && <Checkbox />} Item </> )} </TreeItemContent> </TreeItem> <TreeItem> <TreeItemContent> {( { selectionMode } ) => ( <> {selectionMode !== 'none' && ( <Checkbox /> )} Item </> )} </TreeItemContent> </TreeItem> The states, selectors, and render props for each component used in a `Tree` are documented below. ### Tree# A `Tree` can be targeted with the `.react-aria-Tree` CSS selector, or by overriding with a custom `className`. It supports the following states: | Name | CSS Selector | Description | | --- | --- | --- | | `isEmpty` | `[data-empty]` | Whether the tree has no items and should display its empty state. | | `isFocused` | `[data-focused]` | Whether the tree is currently focused. | | `isFocusVisible` | `[data-focus-visible]` | Whether the tree is currently keyboard focused. | | `selectionMode` | `[data-selection-mode="single | multiple"]` | The type of selection that is allowed in the collection. | | `allowsDragging` | `[data-allows-dragging]` | Whether the tree allows dragging. | | `state` | `—` | State of the tree. | ### TreeItem# A `TreeItem` can be targeted with the `.react-aria-TreeItem` CSS selector, or by overriding with a custom `className`. It supports the following states: | Name | CSS Selector | Description | | --- | --- | --- | | `isExpanded` | `[data-expanded]` | Whether the tree item is expanded. | | `hasChildItems` | `[data-has-child-items]` | Whether the tree item has child tree items. | | `level` | `[data-level="number"]` | What level the tree item has within the tree. | | `isFocusVisibleWithin` | `[data-focus-visible-within]` | Whether the tree item's children have keyboard focus. | | `state` | `—` | The state of the tree. | | `id` | `—` | The unique id of the tree row. | | `isHovered` | `[data-hovered]` | Whether the item is currently hovered with a mouse. | | `isPressed` | `[data-pressed]` | Whether the item is currently in a pressed state. | | `isSelected` | `[data-selected]` | Whether the item is currently selected. | | `isFocused` | `[data-focused]` | Whether the item is currently focused. | | `isFocusVisible` | `[data-focus-visible]` | Whether the item is currently keyboard focused. | | `isDisabled` | `[data-disabled]` | Whether the item is non-interactive, i.e. both selection and actions are disabled and the item may not be focused. Dependent on `disabledKeys` and `disabledBehavior`. | | `selectionMode` | `[data-selection-mode="single | multiple"]` | The type of selection that is allowed in the collection. | | `selectionBehavior` | `—` | The selection behavior for the collection. | | `allowsDragging` | `[data-allows-dragging]` | Whether the item allows dragging. | | `isDragging` | `[data-dragging]` | Whether the item is currently being dragged. | | `isDropTarget` | `[data-drop-target]` | Whether the item is currently an active drop target. | TreeItem also exposes a `--tree-item-level` CSS custom property, which you can use to adjust the indentation. .react-aria-TreeItem { padding-left: calc((var(--tree-item-level) - 1) * 20px); } .react-aria-TreeItem { padding-left: calc((var(--tree-item-level) - 1) * 20px); } .react-aria-TreeItem { padding-left: calc((var(--tree-item-level) - 1) * 20px); } ### TreeItemContent# `TreeItemContent` does not render a DOM node. It supports the following render props: | Name | Description | | --- | --- | | `isExpanded` | Whether the tree item is expanded. | | `hasChildItems` | Whether the tree item has child tree items. | | `level` | What level the tree item has within the tree. | | `isFocusVisibleWithin` | Whether the tree item's children have keyboard focus. | | `state` | The state of the tree. | | `id` | The unique id of the tree row. | | `isHovered` | Whether the item is currently hovered with a mouse. | | `isPressed` | Whether the item is currently in a pressed state. | | `isSelected` | Whether the item is currently selected. | | `isFocused` | Whether the item is currently focused. | | `isFocusVisible` | Whether the item is currently keyboard focused. | | `isDisabled` | Whether the item is non-interactive, i.e. both selection and actions are disabled and the item may not be focused. Dependent on `disabledKeys` and `disabledBehavior`. | | `selectionMode` | The type of selection that is allowed in the collection. | | `selectionBehavior` | The selection behavior for the collection. | | `allowsDragging` | Whether the item allows dragging. | | `isDragging` | Whether the item is currently being dragged. | | `isDropTarget` | Whether the item is currently an active drop target. | ## Advanced customization# * * * ### Contexts# All React Aria Components export a corresponding context that can be used to send props to them from a parent element. This enables you to build your own compositional APIs similar to those found in React Aria Components itself. You can send any prop or ref via context that you could pass to the corresponding component. The local props and ref on the component are merged with the ones passed via context, with the local props taking precedence (following the rules documented in mergeProps). | Component | Context | Props | Ref | | --- | --- | --- | --- | | `Tree` | `TreeContext` | ` TreeProps ` | `HTMLDivElement` | This example shows a component that accepts a `Tree` and a ToggleButton as children, and allows the user to turn selection mode for the tree on and off by pressing the button. import type {SelectionMode} from 'react-aria-components'; import {ToggleButtonContext, TreeContext} from 'react-aria-components'; function Selectable({children}) { let [isSelected, onChange] = React.useState(false); let selectionMode: SelectionMode = isSelected ? 'multiple' : 'none'; return ( <ToggleButtonContext.Provider value={{isSelected, onChange}}> <TreeContext.Provider value={{selectionMode}}> {children} </TreeContext.Provider> </ToggleButtonContext.Provider> ); } import type {SelectionMode} from 'react-aria-components'; import { ToggleButtonContext, TreeContext } from 'react-aria-components'; function Selectable({ children }) { let [isSelected, onChange] = React.useState(false); let selectionMode: SelectionMode = isSelected ? 'multiple' : 'none'; return ( <ToggleButtonContext.Provider value={{ isSelected, onChange }} > <TreeContext.Provider value={{ selectionMode }}> {children} </TreeContext.Provider> </ToggleButtonContext.Provider> ); } import type {SelectionMode} from 'react-aria-components'; import { ToggleButtonContext, TreeContext } from 'react-aria-components'; function Selectable( { children } ) { let [ isSelected, onChange ] = React.useState( false ); let selectionMode: SelectionMode = isSelected ? 'multiple' : 'none'; return ( <ToggleButtonContext.Provider value={{ isSelected, onChange }} > <TreeContext.Provider value={{ selectionMode }} > {children} </TreeContext.Provider> </ToggleButtonContext.Provider> ); } The `Selectable` component can be reused to make the selection mode of any nested `Tree` controlled by a `ToggleButton`. import {ToggleButton} from 'react-aria-components'; <Selectable> <ToggleButton>Select</ToggleButton> <PokemonEvolutionTree /> </Selectable> import {ToggleButton} from 'react-aria-components'; <Selectable> <ToggleButton>Select</ToggleButton> <PokemonEvolutionTree /> </Selectable> import {ToggleButton} from 'react-aria-components'; <Selectable> <ToggleButton> Select </ToggleButton> <PokemonEvolutionTree /> </Selectable> Select Bulbasaur Ivysaur Venusaur Charmander Squirtle Show CSS .react-aria-ToggleButton { margin-bottom: 8px; } .react-aria-ToggleButton { margin-bottom: 8px; } .react-aria-ToggleButton { margin-bottom: 8px; } ### Custom children# Tree passes props to its child components, such as the selection checkboxes, via their associated contexts. These contexts are exported so you can also consume them in your own custom components. This enables you to reuse existing components from your app or component library together with React Aria Components. | Component | Context | Props | Ref | | --- | --- | --- | --- | | `Checkbox` | `CheckboxContext` | ` CheckboxProps ` | `HTMLLabelElement` | | `Button` | `ButtonContext` | ` ButtonProps ` | `HTMLButtonElement` | This example consumes from `CheckboxContext` in an existing styled checkbox component to make it compatible with React Aria Components. The `useContextProps` hook merges the local props and ref with the ones provided via context by Tree. See useCheckbox for more details about the hooks used in this example. import type {CheckboxProps, useContextProps} from 'react-aria-components'; import {CheckboxContext} from 'react-aria-components'; import {useToggleState} from 'react-stately'; import {useCheckbox} from 'react-aria'; const MyCustomCheckbox = React.forwardRef( (props: CheckboxProps, ref: React.ForwardedRef<HTMLInputElement>) => { // Merge the local props and ref with the ones provided via context. [props, ref] = useContextProps(props, ref, CheckboxContext); let state = useToggleState(props); let { inputProps } = useCheckbox(props, state, ref); return <input {...inputProps} ref={ref} />; } ); import type { CheckboxProps, useContextProps } from 'react-aria-components'; import {CheckboxContext} from 'react-aria-components'; import {useToggleState} from 'react-stately'; import {useCheckbox} from 'react-aria'; const MyCustomCheckbox = React.forwardRef( ( props: CheckboxProps, ref: React.ForwardedRef<HTMLInputElement> ) => { // Merge the local props and ref with the ones provided via context. [props, ref] = useContextProps( props, ref, CheckboxContext ); let state = useToggleState(props); let { inputProps } = useCheckbox(props, state, ref); return <input {...inputProps} ref={ref} />; } ); import type { CheckboxProps, useContextProps } from 'react-aria-components'; import {CheckboxContext} from 'react-aria-components'; import {useToggleState} from 'react-stately'; import {useCheckbox} from 'react-aria'; const MyCustomCheckbox = React.forwardRef( ( props: CheckboxProps, ref: React.ForwardedRef< HTMLInputElement > ) => { // Merge the local props and ref with the ones provided via context. [props, ref] = useContextProps( props, ref, CheckboxContext ); let state = useToggleState( props ); let { inputProps } = useCheckbox( props, state, ref ); return ( <input {...inputProps} ref={ref} /> ); } ); Now you can use `MyCustomCheckbox` within a `Tree`, in place of the builtin React Aria Components `Checkbox`. <Tree> <TreeItem> <TreeItemContent> <MyCustomCheckbox slot="selection" /> {/* ... */} </TreeItemContent> </TreeItem> </Tree> <Tree> <TreeItem> <TreeItemContent> <MyCustomCheckbox slot="selection" /> {/* ... */} </TreeItemContent> </TreeItem> </Tree> <Tree> <TreeItem> <TreeItemContent> <MyCustomCheckbox slot="selection" /> {/* ... */} </TreeItemContent> </TreeItem> </Tree> ## Testing# * * * ### Test utils alpha# `@react-aria/test-utils` offers common tree interaction utilities which you may find helpful when writing tests. See here for more information on how to setup these utilities in your tests. Below is the full definition of the tree tester and a sample of how you could use it in your test suite. // Tree.test.ts import {render, within} from '@testing-library/react'; import {User} from '@react-aria/test-utils'; let testUtilUser = new User({ interactionType: 'mouse' }); // ... it('Tree can select a item via keyboard', async function () { // Render your test component/app and initialize the Tree tester let { getByTestId } = render( <Tree data-testid="test-tree" selectionMode="multiple"> ... </Tree> ); let treeTester = testUtilUser.createTester('Tree', { root: getByTestId('test-tree'), interactionType: 'keyboard' }); await treeTester.toggleRowSelection({ row: 0 }); expect(treeTester.selectedRows).toHaveLength(1); expect(within(treeTester.rows[0]).getByRole('checkbox')).toBeChecked(); await treeTester.toggleRowSelection({ row: 1 }); expect(treeTester.selectedRows).toHaveLength(2); expect(within(treeTester.rows[1]).getByRole('checkbox')).toBeChecked(); await treeTester.toggleRowSelection({ row: 0 }); expect(treeTester.selectedRows).toHaveLength(1); expect(within(treeTester.rows[0]).getByRole('checkbox')).not.toBeChecked(); }); // Tree.test.ts import {render, within} from '@testing-library/react'; import {User} from '@react-aria/test-utils'; let testUtilUser = new User({ interactionType: 'mouse' }); // ... it('Tree can select a item via keyboard', async function () { // Render your test component/app and initialize the Tree tester let { getByTestId } = render( <Tree data-testid="test-tree" selectionMode="multiple"> ... </Tree> ); let treeTester = testUtilUser.createTester('Tree', { root: getByTestId('test-tree'), interactionType: 'keyboard' }); await treeTester.toggleRowSelection({ row: 0 }); expect(treeTester.selectedRows).toHaveLength(1); expect(within(treeTester.rows[0]).getByRole('checkbox')) .toBeChecked(); await treeTester.toggleRowSelection({ row: 1 }); expect(treeTester.selectedRows).toHaveLength(2); expect(within(treeTester.rows[1]).getByRole('checkbox')) .toBeChecked(); await treeTester.toggleRowSelection({ row: 0 }); expect(treeTester.selectedRows).toHaveLength(1); expect(within(treeTester.rows[0]).getByRole('checkbox')) .not.toBeChecked(); }); // Tree.test.ts import { render, within } from '@testing-library/react'; import {User} from '@react-aria/test-utils'; let testUtilUser = new User({ interactionType: 'mouse' }); // ... it('Tree can select a item via keyboard', async function () { // Render your test component/app and initialize the Tree tester let { getByTestId } = render( <Tree data-testid="test-tree" selectionMode="multiple" > ... </Tree> ); let treeTester = testUtilUser .createTester( 'Tree', { root: getByTestId( 'test-tree' ), interactionType: 'keyboard' } ); await treeTester .toggleRowSelection({ row: 0 }); expect( treeTester .selectedRows ).toHaveLength(1); expect( within( treeTester.rows[0] ).getByRole( 'checkbox' ) ).toBeChecked(); await treeTester .toggleRowSelection({ row: 1 }); expect( treeTester .selectedRows ).toHaveLength(2); expect( within( treeTester.rows[1] ).getByRole( 'checkbox' ) ).toBeChecked(); await treeTester .toggleRowSelection({ row: 0 }); expect( treeTester .selectedRows ).toHaveLength(1); expect( within( treeTester.rows[0] ).getByRole( 'checkbox' ) ).not.toBeChecked(); }); ### Properties | Name | Type | Description | | --- | --- | --- | | `tree` | `HTMLElement` | Returns the tree. | | `rows` | `HTMLElement[]` | Returns the tree's rows if any. | | `selectedRows` | `HTMLElement[]` | Returns the tree's selected rows if any. | ### Methods | Method | Description | | --- | --- | | `constructor( (opts: TreeTesterOpts )): void` | | | `setInteractionType( (type: UserOpts ['interactionType'] )): void` | Set the interaction type used by the tree tester. | | `findRow( (opts: { rowIndexOrText: number | | string } )): HTMLElement` | Returns a row matching the specified index or text content. | | `toggleRowSelection( (opts: TreeToggleRowOpts )): Promise<void>` | Toggles the selection for the specified tree row. Defaults to using the interaction type set on the tree tester. Note that this will endevor to always add/remove JUST the provided row to the set of selected rows. | | `toggleRowExpansion( (opts: TreeToggleExpansionOpts )): Promise<void>` | Toggles the expansion for the specified tree row. Defaults to using the interaction type set on the tree tester. | | `triggerRowAction( (opts: TreeRowActionOpts )): Promise<void>` | Triggers the action for the specified tree row. Defaults to using the interaction type set on the tree tester. | | `cells( (opts: { element?: HTMLElement } )): HTMLElement[]` | Returns the tree's cells if any. Can be filtered against a specific row if provided via `element`. | --- ## Page: https://react-spectrum.adobe.com/react-aria/Virtualizer.html # Virtualizer A Virtualizer renders a scrollable collection of data using customizable layouts. It supports very large collections by only rendering visible items to the DOM, reusing them as the user scrolls. <table><tbody><tr><th>install</th><td><code>yarn add react-aria-components</code></td></tr><tr><th>version</th><td>1.10.1</td></tr><tr><th>usage</th><td><code><span>import</span> {Virtualizer} <span>from</span> <span>'react-aria-components'</span></code></td></tr></tbody></table> View repository GitHub View package NPM ## Example# * * * import {ListBox, ListBoxItem, ListLayout, Virtualizer} from 'react-aria-components'; let items = []; for (let i = 0; i < 5000; i++) { items.push({ id: i, name: `Item ${i}` }); } function Example() { return ( <Virtualizer layout={ListLayout} layoutOptions={{ rowHeight: 32, padding: 4, gap: 4 }} > <ListBox aria-label="Virtualized ListBox" selectionMode="multiple" items={items} > {(item) => <ListBoxItem>{item.name}</ListBoxItem>} </ListBox> </Virtualizer> ); } import { ListBox, ListBoxItem, ListLayout, Virtualizer } from 'react-aria-components'; let items = []; for (let i = 0; i < 5000; i++) { items.push({ id: i, name: `Item ${i}` }); } function Example() { return ( <Virtualizer layout={ListLayout} layoutOptions={{ rowHeight: 32, padding: 4, gap: 4 }} > <ListBox aria-label="Virtualized ListBox" selectionMode="multiple" items={items} > {(item) => <ListBoxItem>{item.name}</ListBoxItem>} </ListBox> </Virtualizer> ); } import { ListBox, ListBoxItem, ListLayout, Virtualizer } from 'react-aria-components'; let items = []; for ( let i = 0; i < 5000; i++ ) { items.push({ id: i, name: `Item ${i}` }); } function Example() { return ( <Virtualizer layout={ListLayout} layoutOptions={{ rowHeight: 32, padding: 4, gap: 4 }} > <ListBox aria-label="Virtualized ListBox" selectionMode="multiple" items={items} > {(item) => ( <ListBoxItem> {item.name} </ListBoxItem> )} </ListBox> </Virtualizer> ); } Item 0 Item 1 Item 2 Item 3 Item 4 Item 5 Item 6 Item 7 Item 8 Item 9 Item 10 Item 11 ## Features# * * * Virtualized scrolling is a performance optimization for large lists. Instead of rendering all items to the DOM at once, it only renders the visible items, reusing them as the user scrolls. This results in a small number of DOM elements being rendered, reducing memory usage and improving browser layout and rendering performance. * **Integrated** – Works with React Aria ListBox, GridList, Tree, Menu, and Table components. Integrated with React Aria's drag and drop, selection, and table column resizing implementations. * **Custom layouts** – Support for list, grid, waterfall, and table layouts out of the box, with fixed or variable size items. Create a` Layout `subclass to build your own custom layout. * **Accessible** – Persists the focused element in the DOM even when out of view, ensuring keyboard navigation always works. Adds ARIA attributes like `aria-rowindex` to give screen reader users context. ## Anatomy# * * * Collection components such as ListBox, GridList, Tree, Menu, and Table can be virtualized by wrapping them in a < `Virtualizer` \>, and providing a` Layout `object such as` ListLayout `or` GridLayout `. See below for examples of each layout. import {Virtualizer, ListLayout} from 'react-aria-components'; <Virtualizer layout={ListLayout}> <ListBox> {/* ... */} </ListBox> </Virtualizer> import { ListLayout, Virtualizer } from 'react-aria-components'; <Virtualizer layout={ListLayout}> <ListBox> {/* ... */} </ListBox> </Virtualizer> import { ListLayout, Virtualizer } from 'react-aria-components'; <Virtualizer layout={ListLayout} > <ListBox> {/* ... */} </ListBox> </Virtualizer> ### Virtualized components must have a defined size This may be an explicit CSS `width` and `height`, or an implicit size (e.g. percentage or `flex`) bounded by an ancestor element. Without a bounded size, all items will be rendered to the DOM, negating the performance benefits of virtualized scrolling. ## Layouts# * * * Virtualizer uses `Layout` objects to determine the position and size of each item, and provide the list of currently visible items. When using a Virtualizer, all items are positioned by the `Layout`, and CSS layout properties such as flexbox and grid do not apply. ### ListLayout# `ListLayout` supports layout of items in a vertical stack. Rows can be fixed or variable height. When using variable heights, set the `estimatedRowHeight` to a reasonable guess for how tall the rows will be on average. This allows the size of the scrollbar to be calculated. `ListLayout` supports the following options: | Name | Type | Default | Description | | --- | --- | --- | --- | | `rowHeight` | `number` | `48` | The fixed height of a row in px. | | `estimatedRowHeight` | `number` | — | The estimated height of a row, when row heights are variable. | | `headingHeight` | `number` | `48` | The fixed height of a section header in px. | | `estimatedHeadingHeight` | `number` | — | The estimated height of a section header, when the height is variable. | | `loaderHeight` | `number` | `48` | The fixed height of a loader element in px. This loader is specifically for "load more" elements rendered when loading more rows at the root level or inside nested row/sections. | | `dropIndicatorThickness` | `number` | `2` | The thickness of the drop indicator. | | `gap` | `number` | `0` | The gap between items. | | `padding` | `number` | `0` | The padding around the list. | This example shows a GridList with variable height rows due to text wrapping. import {ListLayout, Virtualizer} from 'react-aria-components'; import {MyGridList, MyItem} from './GridList'; function Example() { return ( <Virtualizer layout={ListLayout} layoutOptions={{ estimatedRowHeight: 75, gap: 4, padding: 4 }} > <MyGridList aria-label="Virtualized GridList" selectionMode="multiple" items={items} > {(item) => <MyItem>{item.name}</MyItem>} </MyGridList> </Virtualizer> ); } import { ListLayout, Virtualizer } from 'react-aria-components'; import {MyGridList, MyItem} from './GridList'; function Example() { return ( <Virtualizer layout={ListLayout} layoutOptions={{ estimatedRowHeight: 75, gap: 4, padding: 4 }} > <MyGridList aria-label="Virtualized GridList" selectionMode="multiple" items={items} > {(item) => <MyItem>{item.name}</MyItem>} </MyGridList> </Virtualizer> ); } import { ListLayout, Virtualizer } from 'react-aria-components'; import { MyGridList, MyItem } from './GridList'; function Example() { return ( <Virtualizer layout={ListLayout} layoutOptions={{ estimatedRowHeight: 75, gap: 4, padding: 4 }} > <MyGridList aria-label="Virtualized GridList" selectionMode="multiple" items={items} > {(item) => ( <MyItem> {item.name} </MyItem> )} </MyGridList> </Virtualizer> ); } Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin Lorem ipsum dolor sit amet, consectetur adipiscing Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin sit amet tristique risus. In sit Lorem ipsum Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin sit amet tristique risus. In Lorem ipsum dolor sit amet, consectetur adipiscing ### GridLayout# `GridLayout` supports layout of items in an equal size grid. The items are sized between a minimum and maximum size depending on the width of the container. It supports the following options: | Name | Type | Default | Description | | --- | --- | --- | --- | | `minItemSize` | ` Size ` | `200 x 200` | The minimum item size. | | `maxItemSize` | ` Size ` | `Infinity` | The maximum item size. | | `preserveAspectRatio` | `boolean` | `false` | Whether to preserve the aspect ratio of the `minItemSize`. By default, grid rows may have variable heights. When `preserveAspectRatio` is true, all rows will have equal heights. | | `minSpace` | ` Size ` | `18 x 18` | The minimum space required between items. | | `maxColumns` | `number` | `Infinity` | The maximum number of columns. | | `dropIndicatorThickness` | `number` | `2` | The thickness of the drop indicator. | Make sure to set `layout="grid"` on the `ListBox` or `GridList` component as well so that keyboard navigation behavior is correct. import {GridLayout, Size, Text} from 'react-aria-components'; function Example() { return ( <div className="resizable"> <Virtualizer layout={GridLayout} layoutOptions={{ minItemSize: new Size(100, 140), minSpace: new Size(8, 8) }} > <ListBox layout="grid" aria-label="Virtualized grid layout" selectionMode="multiple" items={albums}> {item => ( <ListBoxItem textValue={item.title}> <img src={item.image} alt="" /> <Text slot="label">{item.title}</Text> <Text slot="description">{item.artist}</Text> </ListBoxItem> )} </ListBox> </Virtualizer> </div> ); } import { GridLayout, Size, Text } from 'react-aria-components'; function Example() { return ( <div className="resizable"> <Virtualizer layout={GridLayout} layoutOptions={{ minItemSize: new Size(100, 140), minSpace: new Size(8, 8) }} > <ListBox layout="grid" aria-label="Virtualized grid layout" selectionMode="multiple" items={albums} > {(item) => ( <ListBoxItem textValue={item.title}> <img src={item.image} alt="" /> <Text slot="label">{item.title}</Text> <Text slot="description">{item.artist}</Text> </ListBoxItem> )} </ListBox> </Virtualizer> </div> ); } import { GridLayout, Size, Text } from 'react-aria-components'; function Example() { return ( <div className="resizable"> <Virtualizer layout={GridLayout} layoutOptions={{ minItemSize: new Size( 100, 140 ), minSpace: new Size( 8, 8 ) }} > <ListBox layout="grid" aria-label="Virtualized grid layout" selectionMode="multiple" items={albums} > {(item) => ( <ListBoxItem textValue={item .title} > <img src={item .image} alt="" /> <Text slot="label"> {item .title} </Text> <Text slot="description"> {item .artist} </Text> </ListBoxItem> )} </ListBox> </Virtualizer> </div> ); } Euphoric EchoesLuna Solstice Neon DreamscapeElectra Skyline Cosmic SerenadeOrion's Symphony Melancholy MelodiesViolet Mistral Rhythmic IllusionsMirage Beats Euphoric EchoesLuna Solstice Neon DreamscapeElectra Skyline Cosmic SerenadeOrion's Symphony Melancholy MelodiesViolet Mistral ### WaterfallLayout# `WaterfallLayout` arranges variable height items in a column layout. The columns are sized between a minimum and maximum size depending on the width of the container. It supports the following options: | Name | Type | Default | Description | | --- | --- | --- | --- | | `minItemSize` | ` Size ` | `200 x 200` | The minimum item size. | | `maxItemSize` | ` Size ` | `Infinity` | The maximum item size. | | `minSpace` | ` Size ` | `18 x 18` | The minimum space required between items. | | `maxColumns` | `number` | `Infinity` | The maximum number of columns. | | `dropIndicatorThickness` | `number` | `2` | The thickness of the drop indicator. | import {Size, Text, WaterfallLayout} from 'react-aria-components'; function Example() { return ( <Virtualizer layout={WaterfallLayout} layoutOptions={{ minItemSize: new Size(150, 150), minSpace: new Size(8, 8) }} > <ListBox layout="grid" aria-label="Virtualized waterfall layout" selectionMode="multiple" items={images} > {(item) => ( <ListBoxItem textValue={item.title}> <img src={item.image} alt="" style={{ aspectRatio: item.aspectRatio }} /> <Text slot="label">{item.title}</Text> <Text slot="description">{item.user}</Text> </ListBoxItem> )} </ListBox> </Virtualizer> ); } import { Size, Text, WaterfallLayout } from 'react-aria-components'; function Example() { return ( <Virtualizer layout={WaterfallLayout} layoutOptions={{ minItemSize: new Size(150, 150), minSpace: new Size(8, 8) }} > <ListBox layout="grid" aria-label="Virtualized waterfall layout" selectionMode="multiple" items={images} > {(item) => ( <ListBoxItem textValue={item.title}> <img src={item.image} alt="" style={{ aspectRatio: item.aspectRatio }} /> <Text slot="label">{item.title}</Text> <Text slot="description">{item.user}</Text> </ListBoxItem> )} </ListBox> </Virtualizer> ); } import { Size, Text, WaterfallLayout } from 'react-aria-components'; function Example() { return ( <Virtualizer layout={WaterfallLayout} layoutOptions={{ minItemSize: new Size( 150, 150 ), minSpace: new Size(8, 8) }} > <ListBox layout="grid" aria-label="Virtualized waterfall layout" selectionMode="multiple" items={images} > {(item) => ( <ListBoxItem textValue={item .title} > <img src={item .image} alt="" style={{ aspectRatio: item .aspectRatio }} /> <Text slot="label"> {item .title} </Text> <Text slot="description"> {item.user} </Text> </ListBoxItem> )} </ListBox> </Virtualizer> ); } A Ficus Lyrata Leaf in the sunlight (2/2) (IG: @clay.banks)Clay Banks beach of Italyalan bajura A winding road in the middle of a forestArtem Stoliar A green and purple aurora over a snow covered forestJanosch Diggelmann A blue and white firework is seen from aboveJanosch Diggelmann A snow covered mountain with a sky backgroundDaniil Silantev "Pastel Sunset"Marek Piwnicki Leave the weight behind! You must make yourself light to strive upwards — to reach the light. (A serene winter landscape featuring a dense collection of bare, white trees.)Simon Berger A snow covered tree with a sky backgroundDaniil Silantev ### TableLayout# `TableLayout` provides layout of items in rows and columns, supporting virtualization of both horizontal and vertical scrolling. It should be used with the Table component. Rows can be fixed or variable height. When using variable heights, set the `estimatedRowHeight` to a reasonable guess for how tall the rows will be on average. This allows the size of the scrollbar to be calculated. `TableLayout` supports the following options: | Name | Type | Default | Description | | --- | --- | --- | --- | | `rowHeight` | `number` | `48` | The fixed height of a row in px. | | `estimatedRowHeight` | `number` | — | The estimated height of a row, when row heights are variable. | | `headingHeight` | `number` | `48` | The fixed height of a section header in px. | | `estimatedHeadingHeight` | `number` | — | The estimated height of a section header, when the height is variable. | | `loaderHeight` | `number` | `48` | The fixed height of a loader element in px. This loader is specifically for "load more" elements rendered when loading more rows at the root level or inside nested row/sections. | | `dropIndicatorThickness` | `number` | `2` | The thickness of the drop indicator. | | `gap` | `number` | `0` | The gap between items. | | `padding` | `number` | `0` | The padding around the list. | import {Cell, Column, Row, Table, TableBody, TableHeader, TableLayout} from 'react-aria-components'; import {MyCheckbox} from './Checkbox'; let rows = []; for (let i = 0; i < 1000; i++) { rows.push({ id: i, foo: `Foo ${i}`, bar: `Bar ${i}`, baz: `Baz ${i}` }); } function Example() { return ( <Virtualizer layout={TableLayout} layoutOptions={{ rowHeight: 32, headingHeight: 32, padding: 4, gap: 4 }} > <Table aria-label="Virtualized Table" selectionMode="multiple"> <TableHeader> <Column width={40} minWidth={0}> <MyCheckbox slot="selection" /> </Column> <Column isRowHeader>Foo</Column> <Column>Bar</Column> <Column>Baz</Column> </TableHeader> <TableBody items={rows}> {(item) => ( <Row style={{ width: 'inherit', height: 'inherit' }}> <Cell> <MyCheckbox slot="selection" /> </Cell> <Cell>{item.foo}</Cell> <Cell>{item.bar}</Cell> <Cell>{item.baz}</Cell> </Row> )} </TableBody> </Table> </Virtualizer> ); } import { Cell, Column, Row, Table, TableBody, TableHeader, TableLayout } from 'react-aria-components'; import {MyCheckbox} from './Checkbox'; let rows = []; for (let i = 0; i < 1000; i++) { rows.push({ id: i, foo: `Foo ${i}`, bar: `Bar ${i}`, baz: `Baz ${i}` }); } function Example() { return ( <Virtualizer layout={TableLayout} layoutOptions={{ rowHeight: 32, headingHeight: 32, padding: 4, gap: 4 }} > <Table aria-label="Virtualized Table" selectionMode="multiple" > <TableHeader> <Column width={40} minWidth={0}> <MyCheckbox slot="selection" /> </Column> <Column isRowHeader>Foo</Column> <Column>Bar</Column> <Column>Baz</Column> </TableHeader> <TableBody items={rows}> {(item) => ( <Row style={{ width: 'inherit', height: 'inherit' }} > <Cell> <MyCheckbox slot="selection" /> </Cell> <Cell>{item.foo}</Cell> <Cell>{item.bar}</Cell> <Cell>{item.baz}</Cell> </Row> )} </TableBody> </Table> </Virtualizer> ); } import { Cell, Column, Row, Table, TableBody, TableHeader, TableLayout } from 'react-aria-components'; import {MyCheckbox} from './Checkbox'; let rows = []; for ( let i = 0; i < 1000; i++ ) { rows.push({ id: i, foo: `Foo ${i}`, bar: `Bar ${i}`, baz: `Baz ${i}` }); } function Example() { return ( <Virtualizer layout={TableLayout} layoutOptions={{ rowHeight: 32, headingHeight: 32, padding: 4, gap: 4 }} > <Table aria-label="Virtualized Table" selectionMode="multiple" > <TableHeader> <Column width={40} minWidth={0} > <MyCheckbox slot="selection" /> </Column> <Column isRowHeader > Foo </Column> <Column> Bar </Column> <Column> Baz </Column> </TableHeader> <TableBody items={rows} > {(item) => ( <Row style={{ width: 'inherit', height: 'inherit' }} > <Cell> <MyCheckbox slot="selection" /> </Cell> <Cell> {item .foo} </Cell> <Cell> {item .bar} </Cell> <Cell> {item .baz} </Cell> </Row> )} </TableBody> </Table> </Virtualizer> ); } Foo Bar Baz Foo 0 Bar 0 Baz 0 Foo 1 Bar 1 Baz 1 Foo 2 Bar 2 Baz 2 Foo 3 Bar 3 Baz 3 Foo 4 Bar 4 Baz 4 Foo 5 Bar 5 Baz 5 Foo 6 Bar 6 Baz 6 Foo 7 Bar 7 Baz 7 Foo 8 Bar 8 Baz 8 Foo 9 Bar 9 Baz 9 Foo 10 Bar 10 Baz 10 ## Advanced: Custom layouts# * * * Custom Virtualizer layouts can be created by extending the `Layout` abstract base class. At a minimum, the `getVisibleLayoutInfos`, `getLayoutInfo`, and `getContentSize` methods must be implemented. You can override the other methods to customize their default behavior. | Method | Description | | --- | --- | | `abstract getVisibleLayoutInfos( (rect: Rect )): LayoutInfo []` | Returns an array of `LayoutInfo` objects which are inside the given rectangle. Should be implemented by subclasses. | | `abstract getLayoutInfo( (key: Key )): LayoutInfo | null` | Returns a `LayoutInfo` for the given key. Should be implemented by subclasses. | | `abstract getContentSize(): Size ` | Returns size of the content. By default, it returns virtualizer's size. | | `shouldInvalidate( (newRect: Rect , , oldRect: Rect )): boolean` | Returns whether the layout should invalidate in response to visible rectangle changes. By default, it only invalidates when the virtualizer's size changes. Return true always to make the layout invalidate while scrolling (e.g. sticky headers). | | `shouldInvalidateLayoutOptions( (newOptions: O, , oldOptions: O )): boolean` | Returns whether the layout should invalidate when the layout options change. By default it invalidates when the object identity changes. Override this method to optimize layout updates based on specific option changes. | | `update( (invalidationContext: InvalidationContext<O> )): void` | This method allows the layout to perform any pre-computation it needs to in order to prepare LayoutInfos for retrieval. Called by the virtualizer before `getVisibleLayoutInfos` or `getLayoutInfo` are called. | | `updateItemSize( (key: Key, , size: Size )): boolean` | Updates the size of the given item. | | `getDropTargetLayoutInfo( (target: ItemDropTarget )): LayoutInfo ` | Returns a `LayoutInfo` for the given drop target. | ### LayoutInfo# Layouts produce `LayoutInfo` objects describing the position, size, and other properties of each item in a collection. Virtualizer requests this information when needed, and uses it to create DOM nodes to display. | Name | Type | Default | Description | | --- | --- | --- | --- | | `type` | `string` | — | The type of element represented by this LayoutInfo. Should match the `type` of the corresponding collection node. | | `key` | `Key` | — | A unique key for this LayoutInfo. Should match the `key` of the corresponding collection node. | | `parentKey` | `Key | null` | — | The key for a parent LayoutInfo, if any. | | `content` | `any | null` | — | Content for this item if it was generated by the layout rather than coming from the Collection. | | `rect` | ` Rect ` | — | The rectangle describing the size and position of this element. | | `estimatedSize` | `boolean` | `false` | Whether the size is estimated. `false` by default. Items with estimated sizes will be measured the first time they are added to the DOM. The estimated size is used to calculate the size and position of the scrollbar. | | `isSticky` | `boolean` | `false` | Whether the layout info sticks to the viewport when scrolling. | | `opacity` | `number` | `1` | The element's opacity. | | `transform` | `string | null` | — | A CSS transform string to apply to the element. `null` by default. | | `zIndex` | `number` | — | The z-index of the element. 0 by default. | | `allowOverflow` | `boolean` | `false` | Whether the element allows its contents to overflow its container. | ### Example# This example implements a horizontally scrolling layout with fixed size items. import {Key, Layout, LayoutInfo, Rect, Size} from 'react-aria-components'; class HorizontalLayout extends Layout { // Determine which items are visible within the given rectangle. getVisibleLayoutInfos(rect: Rect): LayoutInfo[] { let virtualizer = this.virtualizer!; let keys = Array.from(virtualizer.collection.getKeys()); let startIndex = Math.max(0, Math.floor(rect.x / 100)); let endIndex = Math.min(keys.length - 1, Math.ceil(rect.maxX / 100)); let layoutInfos = []; for (let i = startIndex; i <= endIndex; i++) { layoutInfos.push(this.getLayoutInfo(keys[i])); } // Always add persisted keys (e.g. the focused item), even when out of view. for (let key of virtualizer.persistedKeys) { let item = virtualizer.collection.getItem(key); if (item?.index < startIndex) { layoutInfos.unshift(this.getLayoutInfo(key)); } else if (item?.index > endIndex) { layoutInfos.push(this.getLayoutInfo(key)); } } return layoutInfos; } // Provide a LayoutInfo for a specific item. getLayoutInfo(key: Key): LayoutInfo | null { let node = this.virtualizer!.collection.getItem(key); if (!node) { return null; } let rect = new Rect(node.index * 100, 0, 100, 100); return new LayoutInfo(node.type, node.key, rect); } // Provide the total size of all items. getContentSize(): Size { let numItems = this.virtualizer!.collection.size; return new Size(numItems * 100, 100); } } function Example() { let items = []; for (let i = 0; i < 200; i++) { items.push({ id: i, name: `Item ${i}` }); } return ( <Virtualizer layout={HorizontalLayout}> <ListBox aria-label="Favorite animal" items={items} orientation="horizontal" style={{ height: 'fit-content' }} > {(item) => <ListBoxItem className="item">{item.name}</ListBoxItem>} </ListBox> </Virtualizer> ); } import { Key, Layout, LayoutInfo, Rect, Size } from 'react-aria-components'; class HorizontalLayout extends Layout { // Determine which items are visible within the given rectangle. getVisibleLayoutInfos(rect: Rect): LayoutInfo[] { let virtualizer = this.virtualizer!; let keys = Array.from(virtualizer.collection.getKeys()); let startIndex = Math.max(0, Math.floor(rect.x / 100)); let endIndex = Math.min( keys.length - 1, Math.ceil(rect.maxX / 100) ); let layoutInfos = []; for (let i = startIndex; i <= endIndex; i++) { layoutInfos.push(this.getLayoutInfo(keys[i])); } // Always add persisted keys (e.g. the focused item), even when out of view. for (let key of virtualizer.persistedKeys) { let item = virtualizer.collection.getItem(key); if (item?.index < startIndex) { layoutInfos.unshift(this.getLayoutInfo(key)); } else if (item?.index > endIndex) { layoutInfos.push(this.getLayoutInfo(key)); } } return layoutInfos; } // Provide a LayoutInfo for a specific item. getLayoutInfo(key: Key): LayoutInfo | null { let node = this.virtualizer!.collection.getItem(key); if (!node) { return null; } let rect = new Rect(node.index * 100, 0, 100, 100); return new LayoutInfo(node.type, node.key, rect); } // Provide the total size of all items. getContentSize(): Size { let numItems = this.virtualizer!.collection.size; return new Size(numItems * 100, 100); } } function Example() { let items = []; for (let i = 0; i < 200; i++) { items.push({ id: i, name: `Item ${i}` }); } return ( <Virtualizer layout={HorizontalLayout}> <ListBox aria-label="Favorite animal" items={items} orientation="horizontal" style={{ height: 'fit-content' }} > {(item) => ( <ListBoxItem className="item"> {item.name} </ListBoxItem> )} </ListBox> </Virtualizer> ); } import { Key, Layout, LayoutInfo, Rect, Size } from 'react-aria-components'; class HorizontalLayout extends Layout { // Determine which items are visible within the given rectangle. getVisibleLayoutInfos( rect: Rect ): LayoutInfo[] { let virtualizer = this.virtualizer!; let keys = Array .from( virtualizer .collection .getKeys() ); let startIndex = Math .max( 0, Math.floor( rect.x / 100 ) ); let endIndex = Math .min( keys.length - 1, Math.ceil( rect.maxX / 100 ) ); let layoutInfos = []; for ( let i = startIndex; i <= endIndex; i++ ) { layoutInfos.push( this .getLayoutInfo( keys[i] ) ); } // Always add persisted keys (e.g. the focused item), even when out of view. for ( let key of virtualizer .persistedKeys ) { let item = virtualizer .collection .getItem(key); if ( item?.index < startIndex ) { layoutInfos .unshift( this .getLayoutInfo( key ) ); } else if ( item?.index > endIndex ) { layoutInfos.push( this .getLayoutInfo( key ) ); } } return layoutInfos; } // Provide a LayoutInfo for a specific item. getLayoutInfo( key: Key ): LayoutInfo | null { let node = this .virtualizer! .collection .getItem(key); if (!node) { return null; } let rect = new Rect( node.index * 100, 0, 100, 100 ); return new LayoutInfo( node.type, node.key, rect ); } // Provide the total size of all items. getContentSize(): Size { let numItems = this.virtualizer! .collection.size; return new Size( numItems * 100, 100 ); } } function Example() { let items = []; for ( let i = 0; i < 200; i++ ) { items.push({ id: i, name: `Item ${i}` }); } return ( <Virtualizer layout={HorizontalLayout} > <ListBox aria-label="Favorite animal" items={items} orientation="horizontal" style={{ height: 'fit-content' }} > {(item) => ( <ListBoxItem className="item"> {item.name} </ListBoxItem> )} </ListBox> </Virtualizer> ); } Item 0 Item 1 Item 2 Item 3 --- ## Page: https://react-spectrum.adobe.com/react-aria/ColorArea.html # ColorArea A color area allows users to adjust two channels of an RGB, HSL or HSB color value against a two-dimensional gradient background. <table><tbody><tr><th>install</th><td><code>yarn add react-aria-components</code></td></tr><tr><th>version</th><td>1.10.1</td></tr><tr><th>usage</th><td><code><span>import</span> {ColorArea} <span>from</span> <span>'react-aria-components'</span></code></td></tr></tbody></table> View ARIA pattern W3C View repository GitHub View package NPM ## Example# * * * import {ColorArea, ColorThumb} from 'react-aria-components'; <ColorArea> <ColorThumb /> </ColorArea> import {ColorArea, ColorThumb} from 'react-aria-components'; <ColorArea> <ColorThumb /> </ColorArea> import { ColorArea, ColorThumb } from 'react-aria-components'; <ColorArea> <ColorThumb /> </ColorArea> Show CSS .react-aria-ColorArea { width: 192px; height: 192px; border-radius: 4px; flex-shrink: 0; } .react-aria-ColorThumb { border: 2px solid white; box-shadow: 0 0 0 1px black, inset 0 0 0 1px black; width: 20px; height: 20px; border-radius: 50%; box-sizing: border-box; &[data-focus-visible] { width: 24px; height: 24px; } } .react-aria-ColorArea { width: 192px; height: 192px; border-radius: 4px; flex-shrink: 0; } .react-aria-ColorThumb { border: 2px solid white; box-shadow: 0 0 0 1px black, inset 0 0 0 1px black; width: 20px; height: 20px; border-radius: 50%; box-sizing: border-box; &[data-focus-visible] { width: 24px; height: 24px; } } .react-aria-ColorArea { width: 192px; height: 192px; border-radius: 4px; flex-shrink: 0; } .react-aria-ColorThumb { border: 2px solid white; box-shadow: 0 0 0 1px black, inset 0 0 0 1px black; width: 20px; height: 20px; border-radius: 50%; box-sizing: border-box; &[data-focus-visible] { width: 24px; height: 24px; } } ## Features# * * * The <input type="color"> HTML element can be used to build a color picker, however it is very inconsistent across browsers and operating systems and consists of a complete color picker rather than a color area. `ColorArea` helps achieve accessible and touch-friendly color areas that can be styled as needed. * **Customizable** – Support for adjusting two-channel values of an HSL, HSB or RGB color value. * **High quality interactions** – Mouse, touch, and keyboard input is supported via the useMove hook. Pressing the color area moves the thumb to that position. Text selection and touch scrolling are prevented while dragging. * **Accessible** – Announces localized color descriptions for screen reader users (e.g. "dark vibrant blue"). Uses two visually hidden `<input>` elements for mobile screen reader support and HTML form integration. ## Anatomy# * * * A color area consists of a rectangular background area that provides, using a two-dimensional gradient, a visual representation of the range of color values from which a user can select, and a thumb that the user can drag to change the selected color value. Two visually hidden `<input>` elements are used to represent the color channel values to assistive technologies. import {ColorArea, ColorThumb} from 'react-aria-components'; <ColorArea> <ColorThumb /> </ColorArea> import {ColorArea, ColorThumb} from 'react-aria-components'; <ColorArea> <ColorThumb /> </ColorArea> import { ColorArea, ColorThumb } from 'react-aria-components'; <ColorArea> <ColorThumb /> </ColorArea> ## Starter kits# * * * To help kick-start your project, we offer starter kits that include example implementations of all React Aria components with various styling solutions. All components are fully styled, including support for dark mode, high contrast mode, and all UI states. Each starter comes with a pre-configured Storybook that you can experiment with, or use as a starting point for your own component library. Vanilla CSS Download ZIP Preview Tailwind CSS Download ZIP Preview ## Reusable wrappers# * * * If you will use a ColorArea in multiple places in your app, you can wrap all of the pieces into a reusable component. This way, the DOM structure, styling code, and other logic are defined in a single place and reused everywhere to ensure consistency. import type {ColorAreaProps} from 'react-aria-components'; export function MyColorArea(props: ColorAreaProps) { return ( <ColorArea {...props}> <ColorThumb /> </ColorArea> ); } <MyColorArea defaultValue="hsl(30, 100%, 50%)" /> import type {ColorAreaProps} from 'react-aria-components'; export function MyColorArea(props: ColorAreaProps) { return ( <ColorArea {...props}> <ColorThumb /> </ColorArea> ); } <MyColorArea defaultValue="hsl(30, 100%, 50%)" /> import type {ColorAreaProps} from 'react-aria-components'; export function MyColorArea( props: ColorAreaProps ) { return ( <ColorArea {...props} > <ColorThumb /> </ColorArea> ); } <MyColorArea defaultValue="hsl(30, 100%, 50%)" /> ## Value# * * * A ColorArea requires either an uncontrolled default value or a controlled value, provided using the `defaultValue` or `value` props respectively. The value provided to either of these props should be a color string or `Color` object. `xChannel` and `yChannel` props may also be provided to specify which color channels the color area should display, and the direction of each channel in the color gradient. This must be one of the channels included in the color value, for example, for RGB colors, the "red", "green", and "blue" channels are available. For a full list of supported channels, see the Props table below. ### Uncontrolled# By default, color area is uncontrolled, with a default value of white using the RGB color space (`rgb(255, 255, 255)`). You can change the default value using the `defaultValue` prop, and the color area will use the color space of the provided value. <MyColorArea defaultValue="hsb(219, 58%, 93%)" /> <MyColorArea defaultValue="hsb(219, 58%, 93%)" /> <MyColorArea defaultValue="hsb(219, 58%, 93%)" /> If no `xChannel` or `yChannel` is provided, for the RGB color space, the `red` color channel maps to the horizontal axis or `xChannel`, and the `green` color channel maps to the vertical axis or `yChannel`. Similarly, for the HSL and HSB color spaces, the `hue` color channel maps to the horizontal axis or `xChannel`, and the `saturation` color channel maps to the vertical axis or `yChannel`. ### Controlled# In the example below, the `parseColor` function is used to parse the initial color from an HSL string. This is passed to the `value` prop to make the `ColorArea` controlled, and updated in the `onChange` event. import {parseColor} from 'react-aria-components'; function Example() { let [value, setValue] = React.useState(parseColor('hsl(0, 100%, 50%)')); return ( <> <MyColorArea value={value} onChange={setValue} xChannel="saturation" yChannel="lightness" /> <p>Value: {value.toString('hex')}</p> </> ); } import {parseColor} from 'react-aria-components'; function Example() { let [value, setValue] = React.useState( parseColor('hsl(0, 100%, 50%)') ); return ( <> <MyColorArea value={value} onChange={setValue} xChannel="saturation" yChannel="lightness" /> <p>Value: {value.toString('hex')}</p> </> ); } import {parseColor} from 'react-aria-components'; function Example() { let [value, setValue] = React.useState( parseColor( 'hsl(0, 100%, 50%)' ) ); return ( <> <MyColorArea value={value} onChange={setValue} xChannel="saturation" yChannel="lightness" /> <p> Value:{' '} {value.toString( 'hex' )} </p> </> ); } Value: #FF0000 ### HTML forms# ColorArea supports the `xName` and `yName` props for integration with HTML forms. The values will be submitted as numbers between the minimum and maximum value for the corresponding channel in the X and Y direction. <MyColorArea xName="red" yName="green" /> <MyColorArea xName="red" yName="green" /> <MyColorArea xName="red" yName="green" /> ## Events# * * * ColorArea supports two events: `onChange` and `onChangeEnd`. `onChange` is triggered whenever the ColorArea's handle is dragged, and `onChangeEnd` is triggered when the user stops dragging the handle. Both events receive a `Color` object as a parameter. The example below uses `onChange` and `onChangeEnd` to update two separate elements with the ColorArea's value. function Example() { let [currentValue, setCurrentValue] = React.useState( parseColor('hsl(50, 100%, 50%)') ); let [finalValue, setFinalValue] = React.useState(currentValue); return ( <div> <MyColorArea value={currentValue} onChange={setCurrentValue} onChangeEnd={setFinalValue} /> <p>Current value: {currentValue.toString('hsl')}</p> <p>Final value: {finalValue.toString('hsl')}</p> </div> ); } function Example() { let [currentValue, setCurrentValue] = React.useState( parseColor('hsl(50, 100%, 50%)') ); let [finalValue, setFinalValue] = React.useState( currentValue ); return ( <div> <MyColorArea value={currentValue} onChange={setCurrentValue} onChangeEnd={setFinalValue} /> <p>Current value: {currentValue.toString('hsl')}</p> <p>Final value: {finalValue.toString('hsl')}</p> </div> ); } function Example() { let [ currentValue, setCurrentValue ] = React.useState( parseColor( 'hsl(50, 100%, 50%)' ) ); let [ finalValue, setFinalValue ] = React.useState( currentValue ); return ( <div> <MyColorArea value={currentValue} onChange={setCurrentValue} onChangeEnd={setFinalValue} /> <p> Current value: {' '} {currentValue .toString( 'hsl' )} </p> <p> Final value:{' '} {finalValue .toString( 'hsl' )} </p> </div> ); } Current value: hsl(50, 100%, 50%) Final value: hsl(50, 100%, 50%) ## Creating a color picker# * * * To build a fully functional color picker, combine a ColorArea, which adjusts two channels of a color value, with a separate control, like a ColorSlider or ColorWheel, to adjust the value of the third channel. import {ColorSlider, Label, SliderOutput, SliderTrack} from 'react-aria-components'; function Example() { let [color, setColor] = React.useState(parseColor('hsl(50, 100%, 50%)')); return ( <div> <MyColorArea value={color} onChange={setColor} xChannel="saturation" yChannel="lightness" /> <ColorSlider channel="hue" value={color} onChange={setColor} style={{ width: 192, marginTop: 8 }} > <Label /> <SliderOutput /> <SliderTrack> <ColorThumb /> </SliderTrack> </ColorSlider> <p>Current value: {color.toString('hsl')}</p> </div> ); } import { ColorSlider, Label, SliderOutput, SliderTrack } from 'react-aria-components'; function Example() { let [color, setColor] = React.useState( parseColor('hsl(50, 100%, 50%)') ); return ( <div> <MyColorArea value={color} onChange={setColor} xChannel="saturation" yChannel="lightness" /> <ColorSlider channel="hue" value={color} onChange={setColor} style={{ width: 192, marginTop: 8 }} > <Label /> <SliderOutput /> <SliderTrack> <ColorThumb /> </SliderTrack> </ColorSlider> <p>Current value: {color.toString('hsl')}</p> </div> ); } import { ColorSlider, Label, SliderOutput, SliderTrack } from 'react-aria-components'; function Example() { let [color, setColor] = React.useState( parseColor( 'hsl(50, 100%, 50%)' ) ); return ( <div> <MyColorArea value={color} onChange={setColor} xChannel="saturation" yChannel="lightness" /> <ColorSlider channel="hue" value={color} onChange={setColor} style={{ width: 192, marginTop: 8 }} > <Label /> <SliderOutput /> <SliderTrack> <ColorThumb /> </SliderTrack> </ColorSlider> <p> Current value: {' '} {color.toString( 'hsl' )} </p> </div> ); } Hue 50° Current value: hsl(50, 100%, 50%) ## Disabled# * * * A color area can be disabled using the `isDisabled` prop. This prevents the thumb from being focused or dragged. It's up to you to style your color area to appear disabled accordingly. <MyColorArea defaultValue="#ff0" isDisabled /> <MyColorArea defaultValue="#ff0" isDisabled /> <MyColorArea defaultValue="#ff0" isDisabled /> Show CSS .react-aria-ColorArea { &[data-disabled] { background: gray !important; .react-aria-ColorThumb { background: gray !important; } } } .react-aria-ColorArea { &[data-disabled] { background: gray !important; .react-aria-ColorThumb { background: gray !important; } } } .react-aria-ColorArea { &[data-disabled] { background: gray !important; .react-aria-ColorThumb { background: gray !important; } } } ## Labeling# * * * By default, ColorArea provides an `aria-label` for the localized string "Color Picker", which labels the visually hidden `<input>` elements for the two color channels. If you wish to override this with a more specific label, an `aria-label` or `aria-labelledby` prop may be passed to further identify the element to assistive technologies. For example, for a ColorArea that adjusts a background color you might pass the `aria-label` prop, "Background color". If you provide your own `aria-label` or `aria-labelledby`, be sure to localize the string appropriately. <div style={{ display: 'flex', gap: 8, alignItems: 'end', flexWrap: 'wrap' }}> <MyColorArea aria-label="Background color" defaultValue="hsl(0, 100%, 50%)" xChannel="saturation" yChannel="lightness" /> <div> <label id="hsl-aria-labelledby-id">Background color</label> <MyColorArea aria-labelledby="hsl-aria-labelledby-id" defaultValue="hsl(0, 100%, 50%)" xChannel="saturation" yChannel="lightness" /> </div> </div> <div style={{ display: 'flex', gap: 8, alignItems: 'end', flexWrap: 'wrap' }} > <MyColorArea aria-label="Background color" defaultValue="hsl(0, 100%, 50%)" xChannel="saturation" yChannel="lightness" /> <div> <label id="hsl-aria-labelledby-id"> Background color </label> <MyColorArea aria-labelledby="hsl-aria-labelledby-id" defaultValue="hsl(0, 100%, 50%)" xChannel="saturation" yChannel="lightness" /> </div> </div> <div style={{ display: 'flex', gap: 8, alignItems: 'end', flexWrap: 'wrap' }} > <MyColorArea aria-label="Background color" defaultValue="hsl(0, 100%, 50%)" xChannel="saturation" yChannel="lightness" /> <div> <label id="hsl-aria-labelledby-id"> Background color </label> <MyColorArea aria-labelledby="hsl-aria-labelledby-id" defaultValue="hsl(0, 100%, 50%)" xChannel="saturation" yChannel="lightness" /> </div> </div> Background color ### Accessibility# #### Role description# In order to communicate to a screen reader user that the ColorArea adjusts in two dimensions, ColorArea provides an `aria-roledescription`, using the localized string "2D Slider", on each of the visually hidden `<input>` elements. #### Value formatting# The `aria-valuetext` of each `<input>` element within the ColorArea is formatted according to the user's locale automatically. It will include the localized color channel name and current value for each channel, along with a localized description of the selected color (e.g. "dark vibrant blue"). ### Internationalization# In right-to-left languages, color areas should be mirrored. Orientation of the gradient background, positioning of the thumb, and dragging behavior is automatically mirrored by `ColorArea`. ## Props# * * * ### ColorArea# | Name | Type | Description | | --- | --- | --- | | `xName` | `string` | The name of the x channel input element, used when submitting an HTML form. See MDN. | | `yName` | `string` | The name of the y channel input element, used when submitting an HTML form. See MDN. | | `colorSpace` | ` ColorSpace ` | The color space that the color area operates in. The `xChannel` and `yChannel` must be in this color space. If not provided, this defaults to the color space of the `color` or `defaultColor` value. | | `xChannel` | ` ColorChannel ` | Color channel for the horizontal axis. | | `yChannel` | ` ColorChannel ` | Color channel for the vertical axis. | | `isDisabled` | `boolean` | Whether the ColorArea is disabled. | | `value` | `T` | The current value (controlled). | | `defaultValue` | `T` | The default value (uncontrolled). | | `children` | `ReactNode | ( (values: ColorAreaRenderProps & & { defaultChildren: ReactNode | | undefined } )) => ReactNode` | The children of the component. A function may be provided to alter the children based on component state. | | `className` | `string | ( (values: ColorAreaRenderProps & & { defaultClassName: string | | undefined } )) => string` | The CSS className for the element. A function may be provided to compute the class based on component state. | | `style` | `CSSProperties | ( (values: ColorAreaRenderProps & & { defaultStyle: CSSProperties } )) => CSSProperties | undefined` | The inline style for the element. A function may be provided to compute the style based on component state. | Events | Name | Type | Description | | --- | --- | --- | | `onChange` | `( (value: Color )) => void` | Handler that is called when the value changes, as the user drags. | | `onChangeEnd` | `( (value: Color )) => void` | Handler that is called when the user stops dragging. | Layout | Name | Type | Description | | --- | --- | --- | | `slot` | `string | null` | A slot name for the component. Slots allow the component to receive props from a parent component. An explicit `null` value indicates that the local props completely override all props received from a parent. | Accessibility | Name | Type | Description | | --- | --- | --- | | `id` | `string` | The element's unique identifier. See MDN. | | `aria-label` | `string` | Defines a string value that labels the current element. | | `aria-labelledby` | `string` | Identifies the element (or elements) that labels the current element. | | `aria-describedby` | `string` | Identifies the element (or elements) that describes the object. | | `aria-details` | `string` | Identifies the element (or elements) that provide a detailed, extended description for the object. | ### ColorThumb# The `<ColorThumb>` component renders a draggable thumb with a preview of the selected color. Show props | Name | Type | Description | | --- | --- | --- | | `children` | `ReactNode | ( (values: ColorThumbRenderProps & & { defaultChildren: ReactNode | | undefined } )) => ReactNode` | The children of the component. A function may be provided to alter the children based on component state. | | `className` | `string | ( (values: ColorThumbRenderProps & & { defaultClassName: string | | undefined } )) => string` | The CSS className for the element. A function may be provided to compute the class based on component state. | | `style` | `CSSProperties | ( (values: ColorThumbRenderProps & & { defaultStyle: CSSProperties } )) => CSSProperties | undefined` | The inline style for the element. A function may be provided to compute the style based on component state. | Events | Name | Type | Description | | --- | --- | --- | | `onHoverStart` | `( (e: HoverEvent )) => void` | Handler that is called when a hover interaction starts. | | `onHoverEnd` | `( (e: HoverEvent )) => void` | Handler that is called when a hover interaction ends. | | `onHoverChange` | `( (isHovering: boolean )) => void` | Handler that is called when the hover state changes. | ## Styling# * * * React Aria components can be styled in many ways, including using CSS classes, inline styles, utility classes (e.g. Tailwind), CSS-in-JS (e.g. Styled Components), etc. By default, all components include a builtin `className` attribute which can be targeted using CSS selectors. These follow the `react-aria-ComponentName` naming convention. .react-aria-ColorArea { /* ... */ } .react-aria-ColorArea { /* ... */ } .react-aria-ColorArea { /* ... */ } A custom `className` can also be specified on any component. This overrides the default `className` provided by React Aria with your own. <ColorArea className="my-color-area"> {/* ... */} </ColorArea> <ColorArea className="my-color-area"> {/* ... */} </ColorArea> <ColorArea className="my-color-area"> {/* ... */} </ColorArea> In addition, some components support multiple UI states (e.g. focused, placeholder, readonly, etc.). React Aria components expose states using data attributes, which you can target in CSS selectors. For example: .react-aria-ColorThumb[data-dragging] { /* ... */ } .react-aria-ColorThumb[data-focused] { /* ... */ } .react-aria-ColorThumb[data-dragging] { /* ... */ } .react-aria-ColorThumb[data-focused] { /* ... */ } .react-aria-ColorThumb[data-dragging] { /* ... */ } .react-aria-ColorThumb[data-focused] { /* ... */ } The `className` and `style` props also accept functions which receive states for styling. This lets you dynamically determine the classes or styles to apply, which is useful when using utility CSS libraries like Tailwind. <ColorThumb className={({ isDragging }) => isDragging ? 'scale-150' : 'scale-100'} /> <ColorThumb className={({ isDragging }) => isDragging ? 'scale-150' : 'scale-100'} /> <ColorThumb className={( { isDragging } ) => isDragging ? 'scale-150' : 'scale-100'} /> The states, selectors, and render props for each component used in a `ColorArea` are documented below. ### ColorArea# The `ColorArea` component can be targeted with the `.react-aria-ColorArea` CSS selector, or by overriding with a custom `className`. It supports the following states: | Name | CSS Selector | Description | | --- | --- | --- | | `isDisabled` | `[data-disabled]` | Whether the color area is disabled. | | `state` | `—` | State of the color area. | ### ColorThumb# The `ColorThumb` component can be targeted with the `.react-aria-ColorThumb` CSS selector, or by overriding with a custom `className`. It supports the following states: | Name | CSS Selector | Description | | --- | --- | --- | | `color` | `—` | The selected color, excluding the alpha channel. | | `isDragging` | `[data-dragging]` | Whether this thumb is currently being dragged. | | `isHovered` | `[data-hovered]` | Whether the thumb is currently hovered with a mouse. | | `isFocused` | `[data-focused]` | Whether the thumb is currently focused. | | `isFocusVisible` | `[data-focus-visible]` | Whether the thumb is keyboard focused. | | `isDisabled` | `[data-disabled]` | Whether the thumb is disabled. | ## Advanced customization# * * * ### Contexts# All React Aria Components export a corresponding context that can be used to send props to them from a parent element. This enables you to build your own compositional APIs similar to those found in React Aria Components itself. You can send any prop or ref via context that you could pass to the corresponding component. The local props and ref on the component are merged with the ones passed via context, with the local props taking precedence (following the rules documented in mergeProps). | Component | Context | Props | Ref | | --- | --- | --- | --- | | `ColorArea` | `ColorAreaContext` | ` ColorAreaProps ` | `HTMLDivElement` | This example shows a `ColorAreaDescription` component that accepts a color wheel in its children and renders a description element below it. It uses the useId hook to generate a unique id for the description, and associates it with the color wheel via the `aria-describedby` attribute passed to the `ColorAreaContext` provider. import {ColorAreaContext} from 'react-aria-components'; import {useId} from 'react-aria'; interface ColorAreaDescriptionProps { children?: React.ReactNode; description?: string; } function ColorAreaDescription( { children, description }: ColorAreaDescriptionProps ) { let descriptionId = useId(); return ( <div> <ColorAreaContext.Provider value={{ 'aria-describedby': descriptionId }}> {children} </ColorAreaContext.Provider> <small id={descriptionId}>{description}</small> </div> ); } <ColorAreaDescription description="Choose a background color for your profile."> <MyColorArea /> </ColorAreaDescription> import {ColorAreaContext} from 'react-aria-components'; import {useId} from 'react-aria'; interface ColorAreaDescriptionProps { children?: React.ReactNode; description?: string; } function ColorAreaDescription( { children, description }: ColorAreaDescriptionProps ) { let descriptionId = useId(); return ( <div> <ColorAreaContext.Provider value={{ 'aria-describedby': descriptionId }} > {children} </ColorAreaContext.Provider> <small id={descriptionId}>{description}</small> </div> ); } <ColorAreaDescription description="Choose a background color for your profile."> <MyColorArea /> </ColorAreaDescription> import {ColorAreaContext} from 'react-aria-components'; import {useId} from 'react-aria'; interface ColorAreaDescriptionProps { children?: React.ReactNode; description?: string; } function ColorAreaDescription( { children, description }: ColorAreaDescriptionProps ) { let descriptionId = useId(); return ( <div> <ColorAreaContext.Provider value={{ 'aria-describedby': descriptionId }} > {children} </ColorAreaContext.Provider> <small id={descriptionId} > {description} </small> </div> ); } <ColorAreaDescription description="Choose a background color for your profile."> <MyColorArea /> </ColorAreaDescription> Choose a background color for your profile. ### State# ColorArea provides a `ColorAreaState` object to its children via `ColorAreaStateContext`. This can be used to access and manipulate the color area's state. ### Hooks# If you need to customize things even further, such as accessing internal state or customizing DOM structure, you can drop down to the lower level Hook-based API. See useColorArea for more details. --- ## Page: https://react-spectrum.adobe.com/react-aria/ColorField.html # ColorField A color field allows users to edit a hex color or individual color channel value. <table><tbody><tr><th>install</th><td><code>yarn add react-aria-components</code></td></tr><tr><th>version</th><td>1.10.1</td></tr><tr><th>usage</th><td><code><span>import</span> {ColorField} <span>from</span> <span>'react-aria-components'</span></code></td></tr></tbody></table> View ARIA pattern W3C View repository GitHub View package NPM ## Example# * * * import {ColorField, Label, Input} from 'react-aria-components'; <ColorField defaultValue="#ff0"> <Label>Primary Color</Label> <Input /> </ColorField> import { ColorField, Input, Label } from 'react-aria-components'; <ColorField defaultValue="#ff0"> <Label>Primary Color</Label> <Input /> </ColorField> import { ColorField, Input, Label } from 'react-aria-components'; <ColorField defaultValue="#ff0"> <Label> Primary Color </Label> <Input /> </ColorField> Primary Color Show CSS @import "@react-aria/example-theme"; .react-aria-ColorField { display: flex; flex-direction: column; color: var(--text-color); .react-aria-Input { padding: 0.286rem; margin: 0; border: 1px solid var(--border-color); border-radius: 6px; background: var(--field-background); font-size: 1.143rem; color: var(--field-text-color); width: 100%; max-width: 12ch; box-sizing: border-box; &[data-focused] { outline: 2px solid var(--focus-ring-color); outline-offset: -1px; } } } @import "@react-aria/example-theme"; .react-aria-ColorField { display: flex; flex-direction: column; color: var(--text-color); .react-aria-Input { padding: 0.286rem; margin: 0; border: 1px solid var(--border-color); border-radius: 6px; background: var(--field-background); font-size: 1.143rem; color: var(--field-text-color); width: 100%; max-width: 12ch; box-sizing: border-box; &[data-focused] { outline: 2px solid var(--focus-ring-color); outline-offset: -1px; } } } @import "@react-aria/example-theme"; .react-aria-ColorField { display: flex; flex-direction: column; color: var(--text-color); .react-aria-Input { padding: 0.286rem; margin: 0; border: 1px solid var(--border-color); border-radius: 6px; background: var(--field-background); font-size: 1.143rem; color: var(--field-text-color); width: 100%; max-width: 12ch; box-sizing: border-box; &[data-focused] { outline: 2px solid var(--focus-ring-color); outline-offset: -1px; } } } ## Features# * * * The <input type="color"> HTML element can be used to build a color picker, however it is very inconsistent across browsers and operating systems and consists of a complete color picker rather than a single field for editing a hex value or individual color channel. `ColorField` helps achieve accessible color fields that can be styled as needed. * **Interactions** – Supports entering a value using a keyboard, incrementing and decrementing with the arrow keys, and adjusting the value with the scroll wheel. * **Validation** – Keyboard input is validated as the user types so that only valid characters are accepted. Custom client and server-side validation is also supported. * **Accessible** – Follows the spinbutton ARIA pattern. Extensively tested across many devices and assistive technologies to ensure announcements and behaviors are consistent. ## Anatomy# * * * A color field consists of an input element and a label. It also supports optional description and error message elements, which can be used to provide more context about the field, and any validation messages. import {ColorField, FieldError, Input, Label, Text} from 'react-aria-components'; <ColorField> <Label /> <Input /> <Text slot="description" /> <FieldError /> </ColorField> import { ColorField, FieldError, Input, Label, Text } from 'react-aria-components'; <ColorField> <Label /> <Input /> <Text slot="description" /> <FieldError /> </ColorField> import { ColorField, FieldError, Input, Label, Text } from 'react-aria-components'; <ColorField> <Label /> <Input /> <Text slot="description" /> <FieldError /> </ColorField> If there is no visual label, an `aria-label` or `aria-labelledby` prop must be passed instead to identify the element to screen readers. ### Concepts# `ColorField` makes use of the following concepts: Forms Validating and submitting form data, and integrating with form libraries. ### Composed components# A `ColorField` uses the following components, which may also be used standalone or reused in other components. Label A label provides context for an input element. Input An input allows a user to enter a plain text value with a keyboard. ## Starter kits# * * * To help kick-start your project, we offer starter kits that include example implementations of all React Aria components with various styling solutions. All components are fully styled, including support for dark mode, high contrast mode, and all UI states. Each starter comes with a pre-configured Storybook that you can experiment with, or use as a starting point for your own component library. Vanilla CSS Download ZIP Preview Tailwind CSS Download ZIP Preview ## Reusable wrappers# * * * If you will use a ColorField in multiple places in your app, you can wrap all of the pieces into a reusable component. This way, the DOM structure, styling code, and other logic are defined in a single place and reused everywhere to ensure consistency. This example wraps `ColorField` and all of its children together into a single component which accepts a `label` prop, which is passed to the right place. It also shows how to use the `description` slot to render help text, and `FieldError` component to render validation errors. import type {ColorFieldProps, ValidationResult} from 'react-aria-components'; import {FieldError, Text} from 'react-aria-components'; interface MyColorFieldProps extends ColorFieldProps { label?: string; description?: string; errorMessage?: string | ((validation: ValidationResult) => string); } export function MyColorField( { label, description, errorMessage, ...props }: MyColorFieldProps ) { return ( <ColorField {...props}> {label && <Label>{label}</Label>} <Input /> {description && <Text slot="description">{description}</Text>} <FieldError>{errorMessage}</FieldError> </ColorField> ); } <MyColorField label="Color" /> import type { ColorFieldProps, ValidationResult } from 'react-aria-components'; import {FieldError, Text} from 'react-aria-components'; interface MyColorFieldProps extends ColorFieldProps { label?: string; description?: string; errorMessage?: | string | ((validation: ValidationResult) => string); } export function MyColorField( { label, description, errorMessage, ...props }: MyColorFieldProps ) { return ( <ColorField {...props}> {label && <Label>{label}</Label>} <Input /> {description && ( <Text slot="description">{description}</Text> )} <FieldError>{errorMessage}</FieldError> </ColorField> ); } <MyColorField label="Color" /> import type { ColorFieldProps, ValidationResult } from 'react-aria-components'; import { FieldError, Text } from 'react-aria-components'; interface MyColorFieldProps extends ColorFieldProps { label?: string; description?: string; errorMessage?: | string | (( validation: ValidationResult ) => string); } export function MyColorField( { label, description, errorMessage, ...props }: MyColorFieldProps ) { return ( <ColorField {...props} > {label && ( <Label> {label} </Label> )} <Input /> {description && ( <Text slot="description"> {description} </Text> )} <FieldError> {errorMessage} </FieldError> </ColorField> ); } <MyColorField label="Color" /> Color ## Value# * * * A ColorField accepts either a color string or `Color` object as a value. ### Uncontrolled# By default, `ColorField` is uncontrolled. You can set a default value using the `defaultValue` prop. <MyColorField label="Color" defaultValue="#7f007f" /> <MyColorField label="Color" defaultValue="#7f007f" /> <MyColorField label="Color" defaultValue="#7f007f" /> Color ### Controlled# A `ColorField` can be made controlled. The `parseColor` function is used to parse the initial color from a hex string, stored in state. The `value` and `onChange` props are used to update the value in state when the edits the value. import {parseColor} from 'react-aria-components'; function Example() { let [color, setColor] = React.useState(parseColor('#7f007f')); return ( <> <MyColorField label="Color" value={color} onChange={setColor} /> <p>Current color value: {color?.toString('hex')}</p> </> ); } import {parseColor} from 'react-aria-components'; function Example() { let [color, setColor] = React.useState( parseColor('#7f007f') ); return ( <> <MyColorField label="Color" value={color} onChange={setColor} /> <p>Current color value: {color?.toString('hex')}</p> </> ); } import {parseColor} from 'react-aria-components'; function Example() { let [color, setColor] = React.useState( parseColor( '#7f007f' ) ); return ( <> <MyColorField label="Color" value={color} onChange={setColor} /> <p> Current color value:{' '} {color?.toString( 'hex' )} </p> </> ); } Color Current color value: #7F007F ### HTML forms# ColorField supports the `name` prop for integration with HTML forms. The value will be submitted to the server as a hex color string. When a `channel` prop is provided, the value will be submitted as a number instead. <MyColorField label="Color" name="color" /> <MyColorField label="Color" name="color" /> <MyColorField label="Color" name="color" /> Color ## Color channel# * * * By default, ColorField allows the user to edit the color as a hex value. When the `colorSpace` and `channel` props are provided, ColorField displays the value for that channel formatted as a number instead. Rendering multiple ColorFields together can allow a user to edit a color. function Example() { let [color, setColor] = React.useState(parseColor('#7f007f')); return ( <> <div style={{ display: 'flex', gap: 8 }}> <MyColorField label="Hue" value={color} onChange={setColor} colorSpace="hsl" channel="hue" /> <MyColorField label="Saturation" value={color} onChange={setColor} colorSpace="hsl" channel="saturation" /> <MyColorField label="Lightness" value={color} onChange={setColor} colorSpace="hsl" channel="lightness" /> </div> <p>Current color value: {color?.toString('hex')}</p> </> ); } function Example() { let [color, setColor] = React.useState( parseColor('#7f007f') ); return ( <> <div style={{ display: 'flex', gap: 8 }}> <MyColorField label="Hue" value={color} onChange={setColor} colorSpace="hsl" channel="hue" /> <MyColorField label="Saturation" value={color} onChange={setColor} colorSpace="hsl" channel="saturation" /> <MyColorField label="Lightness" value={color} onChange={setColor} colorSpace="hsl" channel="lightness" /> </div> <p>Current color value: {color?.toString('hex')}</p> </> ); } function Example() { let [color, setColor] = React.useState( parseColor( '#7f007f' ) ); return ( <> <div style={{ display: 'flex', gap: 8 }} > <MyColorField label="Hue" value={color} onChange={setColor} colorSpace="hsl" channel="hue" /> <MyColorField label="Saturation" value={color} onChange={setColor} colorSpace="hsl" channel="saturation" /> <MyColorField label="Lightness" value={color} onChange={setColor} colorSpace="hsl" channel="lightness" /> </div> <p> Current color value:{' '} {color?.toString( 'hex' )} </p> </> ); } Hue Saturation Lightness Current color value: #7F007F ## Validation# * * * ColorField supports the `isRequired` prop to ensure the user enters a value, as well as custom validation functions, realtime validation, and server-side validation. It can also be integrated with other form libraries. See the Forms guide to learn more. To display validation errors, add a `<FieldError>` element as a child of the ColorField. This allows you to render error messages from all of the above sources with consistent custom styles. import {Form, FieldError, Button} from 'react-aria-components'; <Form> <ColorField name="color" isRequired> <Label>Color</Label> <Input /> <FieldError /> </ColorField> <Button type="submit">Submit</Button> </Form> import { Button, FieldError, Form } from 'react-aria-components'; <Form> <ColorField name="color" isRequired> <Label>Color</Label> <Input /> <FieldError /> </ColorField> <Button type="submit">Submit</Button> </Form> import { Button, FieldError, Form } from 'react-aria-components'; <Form> <ColorField name="color" isRequired > <Label> Color </Label> <Input /> <FieldError /> </ColorField> <Button type="submit"> Submit </Button> </Form> Color Submit Show CSS .react-aria-ColorField { &[data-invalid] { .react-aria-Input { border-color: var(--invalid-color); } } .react-aria-FieldError { font-size: 12px; color: var(--invalid-color); } } .react-aria-ColorField { &[data-invalid] { .react-aria-Input { border-color: var(--invalid-color); } } .react-aria-FieldError { font-size: 12px; color: var(--invalid-color); } } .react-aria-ColorField { &[data-invalid] { .react-aria-Input { border-color: var(--invalid-color); } } .react-aria-FieldError { font-size: 12px; color: var(--invalid-color); } } By default, `FieldError` displays default validation messages provided by the browser. See Customizing error messages in the Forms guide to learn how to provide your own custom errors. ## Description# * * * The `description` slot can be used to associate additional help text with a color field. <ColorField> <Label>Color</Label> <Input /> <Text slot="description">Enter a background color.</Text></ColorField> <ColorField> <Label>Color</Label> <Input /> <Text slot="description">Enter a background color.</Text></ColorField> <ColorField> <Label>Color</Label> <Input /> <Text slot="description"> Enter a background color. </Text></ColorField> ColorEnter a background color. Show CSS .react-aria-ColorField { [slot=description] { font-size: 12px; } } .react-aria-ColorField { [slot=description] { font-size: 12px; } } .react-aria-ColorField { [slot=description] { font-size: 12px; } } ## Disabled# * * * The `isDisabled` prop can be used prevent the user from editing the value of the color field. <MyColorField label="Disabled" defaultValue="#7f007f" isDisabled /> <MyColorField label="Disabled" defaultValue="#7f007f" isDisabled /> <MyColorField label="Disabled" defaultValue="#7f007f" isDisabled /> Disabled Show CSS .react-aria-ColorField { .react-aria-Input { &[data-disabled] { border-color: var(--border-color-disabled); color: var(--text-color-disabled); } } } .react-aria-ColorField { .react-aria-Input { &[data-disabled] { border-color: var(--border-color-disabled); color: var(--text-color-disabled); } } } .react-aria-ColorField { .react-aria-Input { &[data-disabled] { border-color: var(--border-color-disabled); color: var(--text-color-disabled); } } } ### Read only# The `isReadOnly` prop makes the ColorField's value immutable. Unlike `isDisabled`, the ColorField remains focusable and the contents can still be copied. See the MDN docs for more information. <MyColorField label="Read only" isReadOnly value="#7f007f" /> <MyColorField label="Read only" isReadOnly value="#7f007f" /> <MyColorField label="Read only" isReadOnly value="#7f007f" /> Read only ## Props# * * * ### ColorField# | Name | Type | Default | Description | | --- | --- | --- | --- | | `channel` | ` ColorChannel ` | — | The color channel that this field edits. If not provided, the color is edited as a hex value. | | `colorSpace` | ` ColorSpace ` | — | The color space that the color field operates in if a `channel` prop is provided. If no `channel` is provided, the color field always displays the color as an RGB hex value. | | `isWheelDisabled` | `boolean` | — | Enables or disables changing the value with scroll. | | `value` | `T` | — | The current value (controlled). | | `defaultValue` | `T` | — | The default value (uncontrolled). | | `isDisabled` | `boolean` | — | Whether the input is disabled. | | `isReadOnly` | `boolean` | — | Whether the input can be selected but not changed by the user. | | `isRequired` | `boolean` | — | Whether user input is required on the input before form submission. | | `isInvalid` | `boolean` | — | Whether the input value is invalid. | | `validate` | `( (value: Color | | null )) => ValidationError | true | null | undefined` | — | A function that returns an error message if a given value is invalid. Validation errors are displayed to the user when the form is submitted if `validationBehavior="native"`. For realtime validation, use the `isInvalid` prop instead. | | `autoFocus` | `boolean` | — | Whether the element should receive focus on render. | | `name` | `string` | — | The name of the input element, used when submitting an HTML form. See MDN. | | `validationBehavior` | `'native' | 'aria'` | `'native'` | Whether to use native HTML form validation to prevent form submission when the value is missing or invalid, or mark the field as required or invalid via ARIA. | | `children` | `ReactNode | ( (values: ColorFieldRenderProps & & { defaultChildren: ReactNode | | undefined } )) => ReactNode` | — | The children of the component. A function may be provided to alter the children based on component state. | | `className` | `string | ( (values: ColorFieldRenderProps & & { defaultClassName: string | | undefined } )) => string` | — | The CSS className for the element. A function may be provided to compute the class based on component state. | | `style` | `CSSProperties | ( (values: ColorFieldRenderProps & & { defaultStyle: CSSProperties } )) => CSSProperties | undefined` | — | The inline style for the element. A function may be provided to compute the style based on component state. | Events | Name | Type | Description | | --- | --- | --- | | `onChange` | `( (color: Color | | null )) => void` | Handler that is called when the value changes. | | `onFocus` | `( (e: FocusEvent<Target> )) => void` | Handler that is called when the element receives focus. | | `onBlur` | `( (e: FocusEvent<Target> )) => void` | Handler that is called when the element loses focus. | | `onFocusChange` | `( (isFocused: boolean )) => void` | Handler that is called when the element's focus status changes. | | `onKeyDown` | `( (e: KeyboardEvent )) => void` | Handler that is called when a key is pressed. | | `onKeyUp` | `( (e: KeyboardEvent )) => void` | Handler that is called when a key is released. | | `onCopy` | `ClipboardEventHandler<HTMLInputElement>` | Handler that is called when the user copies text. See MDN. | | `onCut` | `ClipboardEventHandler<HTMLInputElement>` | Handler that is called when the user cuts text. See MDN. | | `onPaste` | `ClipboardEventHandler<HTMLInputElement>` | Handler that is called when the user pastes text. See MDN. | | `onCompositionStart` | `CompositionEventHandler<HTMLInputElement>` | Handler that is called when a text composition system starts a new text composition session. See MDN. | | `onCompositionEnd` | `CompositionEventHandler<HTMLInputElement>` | Handler that is called when a text composition system completes or cancels the current text composition session. See MDN. | | `onCompositionUpdate` | `CompositionEventHandler<HTMLInputElement>` | Handler that is called when a new character is received in the current text composition session. See MDN. | | `onSelect` | `ReactEventHandler<HTMLInputElement>` | Handler that is called when text in the input is selected. See MDN. | | `onBeforeInput` | `FormEventHandler<HTMLInputElement>` | Handler that is called when the input value is about to be modified. See MDN. | | `onInput` | `FormEventHandler<HTMLInputElement>` | Handler that is called when the input value is modified. See MDN. | Layout | Name | Type | Description | | --- | --- | --- | | `slot` | `string | null` | A slot name for the component. Slots allow the component to receive props from a parent component. An explicit `null` value indicates that the local props completely override all props received from a parent. | Accessibility | Name | Type | Description | | --- | --- | --- | | `id` | `string` | The element's unique identifier. See MDN. | | `excludeFromTabOrder` | `boolean` | Whether to exclude the element from the sequential tab order. If true, the element will not be focusable via the keyboard by tabbing. This should be avoided except in rare scenarios where an alternative means of accessing the element or its functionality via the keyboard is available. | | `aria-label` | `string` | Defines a string value that labels the current element. | | `aria-labelledby` | `string` | Identifies the element (or elements) that labels the current element. | | `aria-describedby` | `string` | Identifies the element (or elements) that describes the object. | | `aria-details` | `string` | Identifies the element (or elements) that provide a detailed, extended description for the object. | | `aria-errormessage` | `string` | Identifies the element that provides an error message for the object. | ### Label# A `<Label>` accepts all HTML attributes. ### Input# An `<Input>` accepts all props supported by the `<input>` HTML element. ### FieldError# A `<FieldError>` displays validation errors. Show props | Name | Type | Description | | --- | --- | --- | | `children` | `ReactNode | ( (values: FieldErrorRenderProps & & { defaultChildren: ReactNode | | undefined } )) => ReactNode` | The children of the component. A function may be provided to alter the children based on component state. | | `className` | `string | ( (values: FieldErrorRenderProps & & { defaultClassName: string | | undefined } )) => string` | The CSS className for the element. A function may be provided to compute the class based on component state. | | `style` | `CSSProperties | ( (values: FieldErrorRenderProps & & { defaultStyle: CSSProperties } )) => CSSProperties | undefined` | The inline style for the element. A function may be provided to compute the style based on component state. | Accessibility | Name | Type | Description | | --- | --- | --- | | `id` | `string` | The element's unique identifier. See MDN. | ## Styling# * * * React Aria components can be styled in many ways, including using CSS classes, inline styles, utility classes (e.g. Tailwind), CSS-in-JS (e.g. Styled Components), etc. By default, all components include a builtin `className` attribute which can be targeted using CSS selectors. These follow the `react-aria-ComponentName` naming convention. .react-aria-ColorField { /* ... */ } .react-aria-ColorField { /* ... */ } .react-aria-ColorField { /* ... */ } A custom `className` can also be specified on any component. This overrides the default `className` provided by React Aria with your own. <ColorField className="my-color-field"> {/* ... */} </ColorField> <ColorField className="my-color-field"> {/* ... */} </ColorField> <ColorField className="my-color-field"> {/* ... */} </ColorField> In addition, some components support multiple UI states (e.g. focused, placeholder, readonly, etc.). React Aria components expose states using data attributes, which you can target in CSS selectors. For example: .react-aria-Input[data-focus-visible] { /* ... */ } .react-aria-Input[data-focus-visible] { /* ... */ } .react-aria-Input[data-focus-visible] { /* ... */ } The `className` and `style` props also accept functions which receive states for styling. This lets you dynamically determine the classes or styles to apply, which is useful when using utility CSS libraries like Tailwind. <Input className={({ isFocused }) => isFocused ? 'border-blue-500' : 'border-gray-600'} /> <Input className={({ isFocused }) => isFocused ? 'border-blue-500' : 'border-gray-600'} /> <Input className={( { isFocused } ) => isFocused ? 'border-blue-500' : 'border-gray-600'} /> The states, selectors, and render props for each component used in a `ColorField` are documented below. ### ColorField# A `ColorField` can be targeted with the `.react-aria-ColorField` CSS selector, or by overriding with a custom `className`. It supports the following states: | Name | CSS Selector | Description | | --- | --- | --- | | `isDisabled` | `[data-disabled]` | Whether the color field is disabled. | | `isInvalid` | `[data-invalid]` | Whether the color field is invalid. | | `channel` | `[data-channel="hex | hue | saturation | ..."]` | The color channel that this field edits, or "hex" if no `channel` prop is set. | | `state` | `—` | State of the color field. | ### Label# A `Label` can be targeted with the `.react-aria-Label` CSS selector, or by overriding with a custom `className`. ### Input# An `Input` can be targeted with the `.react-aria-Input` CSS selector, or by overriding with a custom `className`. It supports the following states: | Name | CSS Selector | Description | | --- | --- | --- | | `isHovered` | `[data-hovered]` | Whether the input is currently hovered with a mouse. | | `isFocused` | `[data-focused]` | Whether the input is focused, either via a mouse or keyboard. | | `isFocusVisible` | `[data-focus-visible]` | Whether the input is keyboard focused. | | `isDisabled` | `[data-disabled]` | Whether the input is disabled. | | `isInvalid` | `[data-invalid]` | Whether the input is invalid. | ### Text# The help text elements within a `ColorField` can be targeted with the `[slot=description]` and `[slot=errorMessage]` CSS selectors, or by adding a custom `className`. ### FieldError# A `FieldError` can be targeted with the `.react-aria-FieldError` CSS selector, or by overriding with a custom `className`. It supports the following render props: | Name | Description | | --- | --- | | `isInvalid` | Whether the input value is invalid. | | `validationErrors` | The current error messages for the input if it is invalid, otherwise an empty array. | | `validationDetails` | The native validation details for the input. | ## Advanced customization# * * * ### Composition# If you need to customize one of the components within a `ColorField`, such as `Label` or `Input`, in many cases you can create a wrapper component. This lets you customize the props passed to the component. function MyInput(props) { return <Input {...props} className="my-input" /> } function MyInput(props) { return <Input {...props} className="my-input" /> } function MyInput(props) { return ( <Input {...props} className="my-input" /> ); } ### Contexts# All React Aria Components export a corresponding context that can be used to send props to them from a parent element. This enables you to build your own compositional APIs similar to those found in React Aria Components itself. You can send any prop or ref via context that you could pass to the corresponding component. The local props and ref on the component are merged with the ones passed via context, with the local props taking precedence (following the rules documented in mergeProps). | Component | Context | Props | Ref | | --- | --- | --- | --- | | `ColorField` | `ColorFieldContext` | ` ColorFieldProps ` | `HTMLDivElement` | This example shows a `FieldGroup` component that renders a group of color fields with a title. The entire group can be marked as read only via the `isReadOnly` prop, which is passed to all child color fields via the `ColorFieldContext` provider. import {ColorFieldContext} from 'react-aria-components'; interface FieldGroupProps { title?: string, children?: React.ReactNode, isReadOnly?: boolean } function FieldGroup({title, children, isReadOnly}: FieldGroupProps) { return ( <fieldset> <legend>{title}</legend> <ColorFieldContext.Provider value={{isReadOnly}}> {children} </ColorFieldContext.Provider> </fieldset> ); } <FieldGroup title="Colors" isReadOnly> <MyColorField label="Background" defaultValue="#fff" /> <MyColorField label="Foreground" defaultValue="#000" /> </FieldGroup> import {ColorFieldContext} from 'react-aria-components'; interface FieldGroupProps { title?: string; children?: React.ReactNode; isReadOnly?: boolean; } function FieldGroup( { title, children, isReadOnly }: FieldGroupProps ) { return ( <fieldset> <legend>{title}</legend> <ColorFieldContext.Provider value={{ isReadOnly }}> {children} </ColorFieldContext.Provider> </fieldset> ); } <FieldGroup title="Colors" isReadOnly> <MyColorField label="Background" defaultValue="#fff" /> <MyColorField label="Foreground" defaultValue="#000" /> </FieldGroup> import {ColorFieldContext} from 'react-aria-components'; interface FieldGroupProps { title?: string; children?: React.ReactNode; isReadOnly?: boolean; } function FieldGroup( { title, children, isReadOnly }: FieldGroupProps ) { return ( <fieldset> <legend> {title} </legend> <ColorFieldContext.Provider value={{ isReadOnly }} > {children} </ColorFieldContext.Provider> </fieldset> ); } <FieldGroup title="Colors" isReadOnly > <MyColorField label="Background" defaultValue="#fff" /> <MyColorField label="Foreground" defaultValue="#000" /> </FieldGroup> Colors Background Foreground Show CSS fieldset { padding: 1.5em; width: fit-content; } fieldset { padding: 1.5em; width: fit-content; } fieldset { padding: 1.5em; width: fit-content; } ### Custom children# ColorField passes props to its child components, such as the label and input, via their associated contexts. These contexts are exported so you can also consume them in your own custom components. This enables you to reuse existing components from your app or component library together with React Aria Components. | Component | Context | Props | Ref | | --- | --- | --- | --- | | `Label` | `LabelContext` | ` LabelProps ` | `HTMLLabelElement` | | `Input` | `InputContext` | ` InputProps ` | `HTMLInputElement` | | `Text` | `TextContext` | ` TextProps ` | `HTMLElement` | This example consumes from `LabelContext` in an existing styled label component to make it compatible with React Aria Components. The `useContextProps` hook merges the local props and ref with the ones provided via context by ColorField. import type {LabelProps} from 'react-aria-components'; import {LabelContext, useContextProps} from 'react-aria-components'; const MyCustomLabel = React.forwardRef( (props: LabelProps, ref: React.ForwardedRef<HTMLLabelElement>) => { // Merge the local props and ref with the ones provided via context. [props, ref] = useContextProps(props, ref, LabelContext); // ... your existing Label component return <label {...props} ref={ref} />; } ); import type {LabelProps} from 'react-aria-components'; import { LabelContext, useContextProps } from 'react-aria-components'; const MyCustomLabel = React.forwardRef( ( props: LabelProps, ref: React.ForwardedRef<HTMLLabelElement> ) => { // Merge the local props and ref with the ones provided via context. [props, ref] = useContextProps( props, ref, LabelContext ); // ... your existing Label component return <label {...props} ref={ref} />; } ); import type {LabelProps} from 'react-aria-components'; import { LabelContext, useContextProps } from 'react-aria-components'; const MyCustomLabel = React.forwardRef( ( props: LabelProps, ref: React.ForwardedRef< HTMLLabelElement > ) => { // Merge the local props and ref with the ones provided via context. [props, ref] = useContextProps( props, ref, LabelContext ); // ... your existing Label component return ( <label {...props} ref={ref} /> ); } ); Now you can use `MyCustomLabel` within a `ColorField`, in place of the builtin React Aria Components `Label`. <ColorField> <MyCustomLabel>Value</MyCustomLabel> <Input /> </ColorField> <ColorField> <MyCustomLabel>Value</MyCustomLabel> <Input /> </ColorField> <ColorField> <MyCustomLabel> Value </MyCustomLabel> <Input /> </ColorField> ### State# ColorField provides a `ColorFieldState` object to its children via `ColorFieldStateContext`. This can be used to access and manipulate the ColorField's state. ### Hooks# If you need to customize things even further, such as accessing internal state or customizing DOM structure, you can drop down to the lower level Hook-based API. See useColorField for more details. --- ## Page: https://react-spectrum.adobe.com/react-aria/ColorPicker.html # ColorPicker A ColorPicker synchronizes a color value between multiple React Aria color components. It simplifies building color pickers with customizable layouts via composition. <table><tbody><tr><th>install</th><td><code>yarn add react-aria-components</code></td></tr><tr><th>version</th><td>1.10.1</td></tr><tr><th>usage</th><td><code><span>import</span> {ColorPicker} <span>from</span> <span>'react-aria-components'</span></code></td></tr></tbody></table> View repository GitHub View package NPM ## Example# * * * import {Button, ColorPicker, Dialog, DialogTrigger, Popover} from 'react-aria-components'; import {MyColorSwatch} from './ColorSwatch'; import {MyColorSlider} from './ColorSlider'; import {MyColorArea} from './ColorArea'; import {MyColorField} from './ColorField'; <ColorPicker defaultValue="#5100FF"> <DialogTrigger> <Button className="color-picker"> <MyColorSwatch /> <span>Fill color</span> </Button> <Popover placement="bottom start"> <Dialog className="color-picker-dialog"> <MyColorArea colorSpace="hsb" xChannel="saturation" yChannel="brightness" /> <MyColorSlider colorSpace="hsb" channel="hue" /> <MyColorField label="Hex" /> </Dialog> </Popover> </DialogTrigger> </ColorPicker> import { Button, ColorPicker, Dialog, DialogTrigger, Popover } from 'react-aria-components'; import {MyColorSwatch} from './ColorSwatch'; import {MyColorSlider} from './ColorSlider'; import {MyColorArea} from './ColorArea'; import {MyColorField} from './ColorField'; <ColorPicker defaultValue="#5100FF"> <DialogTrigger> <Button className="color-picker"> <MyColorSwatch /> <span>Fill color</span> </Button> <Popover placement="bottom start"> <Dialog className="color-picker-dialog"> <MyColorArea colorSpace="hsb" xChannel="saturation" yChannel="brightness" /> <MyColorSlider colorSpace="hsb" channel="hue" /> <MyColorField label="Hex" /> </Dialog> </Popover> </DialogTrigger> </ColorPicker> import { Button, ColorPicker, Dialog, DialogTrigger, Popover } from 'react-aria-components'; import {MyColorSwatch} from './ColorSwatch'; import {MyColorSlider} from './ColorSlider'; import {MyColorArea} from './ColorArea'; import {MyColorField} from './ColorField'; <ColorPicker defaultValue="#5100FF"> <DialogTrigger> <Button className="color-picker"> <MyColorSwatch /> <span> Fill color </span> </Button> <Popover placement="bottom start"> <Dialog className="color-picker-dialog"> <MyColorArea colorSpace="hsb" xChannel="saturation" yChannel="brightness" /> <MyColorSlider colorSpace="hsb" channel="hue" /> <MyColorField label="Hex" /> </Dialog> </Popover> </DialogTrigger> </ColorPicker> Fill color Show CSS @import "@react-aria/example-theme"; .color-picker { background: none; border: none; padding: 0; display: flex; align-items: center; gap: 8px; outline: none; border-radius: 4px; appearance: none; vertical-align: middle; font-size: 1rem; color: var(--text-color); &[data-focus-visible] { outline: 2px solid var(--focus-ring-color); outline-offset: 2px; } } .color-picker-dialog { outline: none; padding: 15px; display: flex; flex-direction: column; gap: 8px; min-width: 192px; max-height: inherit; box-sizing: border-box; overflow: auto; } @import "@react-aria/example-theme"; .color-picker { background: none; border: none; padding: 0; display: flex; align-items: center; gap: 8px; outline: none; border-radius: 4px; appearance: none; vertical-align: middle; font-size: 1rem; color: var(--text-color); &[data-focus-visible] { outline: 2px solid var(--focus-ring-color); outline-offset: 2px; } } .color-picker-dialog { outline: none; padding: 15px; display: flex; flex-direction: column; gap: 8px; min-width: 192px; max-height: inherit; box-sizing: border-box; overflow: auto; } @import "@react-aria/example-theme"; .color-picker { background: none; border: none; padding: 0; display: flex; align-items: center; gap: 8px; outline: none; border-radius: 4px; appearance: none; vertical-align: middle; font-size: 1rem; color: var(--text-color); &[data-focus-visible] { outline: 2px solid var(--focus-ring-color); outline-offset: 2px; } } .color-picker-dialog { outline: none; padding: 15px; display: flex; flex-direction: column; gap: 8px; min-width: 192px; max-height: inherit; box-sizing: border-box; overflow: auto; } ## Features# * * * The <input type="color"> HTML element can be used to build a color picker, however it is very inconsistent across browsers and operating systems and does not allow customization. `ColorPicker` helps achieve accessible color pickers that can be styled as needed. * **Customizable** – Support for rendering any color picker UI by placing components like ColorArea, ColorSlider, ColorWheel, ColorField, and ColorSwatchPicker inside. These can be arranged in any desired combination or layout, with or without a popover. * **Accessible** – All color components announce localized color descriptions for screen reader users (e.g. "dark vibrant blue"). ## Anatomy# * * * A color picker does not render any UI by itself. You can render any combination of color components as children of a color picker, including ColorArea, ColorSlider, ColorWheel, ColorField, ColorSwatch, and ColorSwatchPicker. `ColorPicker` manages the state of the selected color, and coordinates the value between all of the components inside it. import {ColorPicker} from 'react-aria-components'; <ColorPicker> {/* Color components here */} </ColorPicker> import {ColorPicker} from 'react-aria-components'; <ColorPicker> {/* Color components here */} </ColorPicker> import {ColorPicker} from 'react-aria-components'; <ColorPicker> {/* Color components here */} </ColorPicker> ### Composed components# A `ColorPicker` uses the following components, which may also be used standalone or reused in other components. ColorArea A color area allows users to adjust two channels of a color value. ColorSlider A color slider allows users to adjust an individual channel of a color value. ColorWheel A color wheel allows users to adjust the hue of a color value on a circular track. ColorField A color field allows users to edit a hex color or individual color channel value. ColorSwatch A ColorSwatch displays a preview of a selected color. ColorSwatchPicker A ColorSwatchPicker displays a list of color swatches and allows a user to select one of them. ## Starter kits# * * * To help kick-start your project, we offer starter kits that include example implementations of all React Aria components with various styling solutions. All components are fully styled, including support for dark mode, high contrast mode, and all UI states. Each starter comes with a pre-configured Storybook that you can experiment with, or use as a starting point for your own component library. Vanilla CSS Download ZIP Preview Tailwind CSS Download ZIP Preview ## Reusable wrappers# * * * If you will use a ColorPicker in multiple places in your app, you can wrap all of the pieces into a reusable component. This way, the DOM structure, styling code, and other logic are defined in a single place and reused everywhere to ensure consistency. This example wraps `ColorPicker` and all of its children together into a single component. It uses the reusable wrappers for ColorSwatch, ColorArea, ColorSlider, and ColorField created on their corresponding documentation pages. Custom children can also be provided to customize the layout within the popover. import type {ColorPickerProps} from 'react-aria-components'; import {Button, ColorPicker, Dialog, DialogTrigger, Popover} from 'react-aria-components'; import {MyColorSwatch} from './ColorSwatch'; import {MyColorArea} from './ColorArea'; import {MyColorSlider} from './ColorSlider'; import {MyColorField} from './ColorField'; interface MyColorPickerProps extends ColorPickerProps { label?: string; children?: React.ReactNode; } function MyColorPicker({ label, children, ...props }: MyColorPickerProps) { return ( <ColorPicker {...props}> <DialogTrigger> <Button className="color-picker"> <MyColorSwatch /> <span>{label}</span> </Button> <Popover placement="bottom start"> <Dialog className="color-picker-dialog"> {children || ( <> <MyColorArea colorSpace="hsb" xChannel="saturation" yChannel="brightness" /> <MyColorSlider colorSpace="hsb" channel="hue" /> <MyColorField label="Hex" /> </> )} </Dialog> </Popover> </DialogTrigger> </ColorPicker> ); } <MyColorPicker label="Fill color" defaultValue="#f00" /> import type {ColorPickerProps} from 'react-aria-components'; import { Button, ColorPicker, Dialog, DialogTrigger, Popover } from 'react-aria-components'; import {MyColorSwatch} from './ColorSwatch'; import {MyColorArea} from './ColorArea'; import {MyColorSlider} from './ColorSlider'; import {MyColorField} from './ColorField'; interface MyColorPickerProps extends ColorPickerProps { label?: string; children?: React.ReactNode; } function MyColorPicker( { label, children, ...props }: MyColorPickerProps ) { return ( <ColorPicker {...props}> <DialogTrigger> <Button className="color-picker"> <MyColorSwatch /> <span>{label}</span> </Button> <Popover placement="bottom start"> <Dialog className="color-picker-dialog"> {children || ( <> <MyColorArea colorSpace="hsb" xChannel="saturation" yChannel="brightness" /> <MyColorSlider colorSpace="hsb" channel="hue" /> <MyColorField label="Hex" /> </> )} </Dialog> </Popover> </DialogTrigger> </ColorPicker> ); } <MyColorPicker label="Fill color" defaultValue="#f00" /> import type {ColorPickerProps} from 'react-aria-components'; import { Button, ColorPicker, Dialog, DialogTrigger, Popover } from 'react-aria-components'; import {MyColorSwatch} from './ColorSwatch'; import {MyColorArea} from './ColorArea'; import {MyColorSlider} from './ColorSlider'; import {MyColorField} from './ColorField'; interface MyColorPickerProps extends ColorPickerProps { label?: string; children?: React.ReactNode; } function MyColorPicker( { label, children, ...props }: MyColorPickerProps ) { return ( <ColorPicker {...props} > <DialogTrigger> <Button className="color-picker"> <MyColorSwatch /> <span> {label} </span> </Button> <Popover placement="bottom start"> <Dialog className="color-picker-dialog"> {children || ( <> <MyColorArea colorSpace="hsb" xChannel="saturation" yChannel="brightness" /> <MyColorSlider colorSpace="hsb" channel="hue" /> <MyColorField label="Hex" /> </> )} </Dialog> </Popover> </DialogTrigger> </ColorPicker> ); } <MyColorPicker label="Fill color" defaultValue="#f00" /> Fill color ## Value# * * * A ColorPicker requires either an uncontrolled default value or a controlled value, provided using the `defaultValue` or `value` props respectively. The value provided to either of these props should be a color string or `Color` object. ### Uncontrolled# By default, `ColorPicker` is uncontrolled. You can set a default value using the `defaultValue` prop. <MyColorPicker label="Color" defaultValue="hsl(25, 100%, 50%)" /> <MyColorPicker label="Color" defaultValue="hsl(25, 100%, 50%)" /> <MyColorPicker label="Color" defaultValue="hsl(25, 100%, 50%)" /> Color ### Controlled# In the example below, the `parseColor` function is used to parse the initial color from a HSL string so that `value`'s type remains consistent. import {parseColor} from 'react-aria-components'; function Example() { let [value, setValue] = React.useState(parseColor('hsl(25, 100%, 50%)')); return ( <MyColorPicker label="Color" value={value} onChange={setValue} /> ); } import {parseColor} from 'react-aria-components'; function Example() { let [value, setValue] = React.useState( parseColor('hsl(25, 100%, 50%)') ); return ( <MyColorPicker label="Color" value={value} onChange={setValue} /> ); } import {parseColor} from 'react-aria-components'; function Example() { let [value, setValue] = React.useState( parseColor( 'hsl(25, 100%, 50%)' ) ); return ( <MyColorPicker label="Color" value={value} onChange={setValue} /> ); } Color ## Events# * * * ColorPicker accepts an `onChange` prop which is triggered whenever the value is edited by the user. It receives a `Color` object as a parameter. The example below uses `onChange` to update a separate element with the color value as a string. function Example() { let [value, setValue] = React.useState(parseColor('hsl(50, 100%, 50%)')); return ( <div> <MyColorPicker label="Color" value={value} onChange={setValue} /> <p>Selected color: {value.toString('hsl')}</p> </div> ); } function Example() { let [value, setValue] = React.useState( parseColor('hsl(50, 100%, 50%)') ); return ( <div> <MyColorPicker label="Color" value={value} onChange={setValue} /> <p>Selected color: {value.toString('hsl')}</p> </div> ); } function Example() { let [value, setValue] = React.useState( parseColor( 'hsl(50, 100%, 50%)' ) ); return ( <div> <MyColorPicker label="Color" value={value} onChange={setValue} /> <p> Selected color: {' '} {value.toString( 'hsl' )} </p> </div> ); } Color Selected color: hsl(50, 100%, 50%) ## Examples# * * * ### Channel sliders# This example uses ColorSlider to allow a user to adjust each channel of a color value, with a Select to switch between color spaces. import type {ColorSpace} from 'react-aria-components'; import {getColorChannels} from 'react-aria-components'; import {MyColorSlider} from './ColorSlider'; import {MyItem, MySelect} from './Select'; function Example() { let [space, setSpace] = React.useState<ColorSpace>('rgb'); return ( <MyColorPicker label="Fill color" defaultValue="#184"> <MySelect aria-label="Color space" selectedKey={space} onSelectionChange={(s) => setSpace(s as ColorSpace)} > <MyItem id="rgb">RGB</MyItem> <MyItem id="hsl">HSL</MyItem> <MyItem id="hsb">HSB</MyItem> </MySelect> {getColorChannels(space).map((channel) => ( <MyColorSlider key={channel} colorSpace={space} channel={channel} /> ))} <MyColorSlider channel="alpha" /> </MyColorPicker> ); } import type {ColorSpace} from 'react-aria-components'; import {getColorChannels} from 'react-aria-components'; import {MyColorSlider} from './ColorSlider'; import {MyItem, MySelect} from './Select'; function Example() { let [space, setSpace] = React.useState<ColorSpace>('rgb'); return ( <MyColorPicker label="Fill color" defaultValue="#184"> <MySelect aria-label="Color space" selectedKey={space} onSelectionChange={(s) => setSpace(s as ColorSpace)} > <MyItem id="rgb">RGB</MyItem> <MyItem id="hsl">HSL</MyItem> <MyItem id="hsb">HSB</MyItem> </MySelect> {getColorChannels(space).map((channel) => ( <MyColorSlider key={channel} colorSpace={space} channel={channel} /> ))} <MyColorSlider channel="alpha" /> </MyColorPicker> ); } import type {ColorSpace} from 'react-aria-components'; import {getColorChannels} from 'react-aria-components'; import {MyColorSlider} from './ColorSlider'; import { MyItem, MySelect } from './Select'; function Example() { let [space, setSpace] = React.useState< ColorSpace >('rgb'); return ( <MyColorPicker label="Fill color" defaultValue="#184" > <MySelect aria-label="Color space" selectedKey={space} onSelectionChange={(s) => setSpace( s as ColorSpace )} > <MyItem id="rgb"> RGB </MyItem> <MyItem id="hsl"> HSL </MyItem> <MyItem id="hsb"> HSB </MyItem> </MySelect> {getColorChannels( space ).map( (channel) => ( <MyColorSlider key={channel} colorSpace={space} channel={channel} /> ) )} <MyColorSlider channel="alpha" /> </MyColorPicker> ); } Fill color ### Color wheel# This example combines a ColorWheel and ColorArea to build an HSB color picker. import {MyColorWheel} from './ColorWheel'; import {MyColorArea} from './ColorArea'; <MyColorPicker label="Stroke color" defaultValue="#345"> <MyColorWheel /> <MyColorArea colorSpace="hsb" xChannel="saturation" yChannel="brightness" style={{ width: '100px', height: '100px', position: 'absolute', top: 'calc(50% - 50px)', left: 'calc(50% - 50px)' }} /> </MyColorPicker> import {MyColorWheel} from './ColorWheel'; import {MyColorArea} from './ColorArea'; <MyColorPicker label="Stroke color" defaultValue="#345"> <MyColorWheel /> <MyColorArea colorSpace="hsb" xChannel="saturation" yChannel="brightness" style={{ width: '100px', height: '100px', position: 'absolute', top: 'calc(50% - 50px)', left: 'calc(50% - 50px)' }} /> </MyColorPicker> import {MyColorWheel} from './ColorWheel'; import {MyColorArea} from './ColorArea'; <MyColorPicker label="Stroke color" defaultValue="#345" > <MyColorWheel /> <MyColorArea colorSpace="hsb" xChannel="saturation" yChannel="brightness" style={{ width: '100px', height: '100px', position: 'absolute', top: 'calc(50% - 50px)', left: 'calc(50% - 50px)' }} /> </MyColorPicker> Stroke color ### Channel fields# This example uses ColorField to allow a user to edit the value of each color channel as a number, along with a Select to switch between color spaces. import type {ColorSpace} from 'react-aria-components'; import {getColorChannels} from 'react-aria-components'; import {MyColorArea} from './ColorArea'; import {MyColorSlider} from './ColorSlider'; import {MyItem, MySelect} from './Select'; import {MyColorField} from './ColorField'; function Example() { let [space, setSpace] = React.useState<ColorSpace>('rgb'); return ( <MyColorPicker label="Color" defaultValue="#f80"> <MyColorArea colorSpace="hsb" xChannel="saturation" yChannel="brightness" /> <MyColorSlider colorSpace="hsb" channel="hue" /> <MySelect aria-label="Color space" selectedKey={space} onSelectionChange={(s) => setSpace(s as ColorSpace)} > <MyItem id="rgb">RGB</MyItem> <MyItem id="hsl">HSL</MyItem> <MyItem id="hsb">HSB</MyItem> </MySelect> <div style={{ display: 'flex', gap: 4, width: 192 }}> {getColorChannels(space).map((channel) => ( <MyColorField key={channel} colorSpace={space} channel={channel} style={{ flex: 1 }} /> ))} </div> </MyColorPicker> ); } import type {ColorSpace} from 'react-aria-components'; import {getColorChannels} from 'react-aria-components'; import {MyColorArea} from './ColorArea'; import {MyColorSlider} from './ColorSlider'; import {MyItem, MySelect} from './Select'; import {MyColorField} from './ColorField'; function Example() { let [space, setSpace] = React.useState<ColorSpace>('rgb'); return ( <MyColorPicker label="Color" defaultValue="#f80"> <MyColorArea colorSpace="hsb" xChannel="saturation" yChannel="brightness" /> <MyColorSlider colorSpace="hsb" channel="hue" /> <MySelect aria-label="Color space" selectedKey={space} onSelectionChange={(s) => setSpace(s as ColorSpace)} > <MyItem id="rgb">RGB</MyItem> <MyItem id="hsl">HSL</MyItem> <MyItem id="hsb">HSB</MyItem> </MySelect> <div style={{ display: 'flex', gap: 4, width: 192 }}> {getColorChannels(space).map((channel) => ( <MyColorField key={channel} colorSpace={space} channel={channel} style={{ flex: 1 }} /> ))} </div> </MyColorPicker> ); } import type {ColorSpace} from 'react-aria-components'; import {getColorChannels} from 'react-aria-components'; import {MyColorArea} from './ColorArea'; import {MyColorSlider} from './ColorSlider'; import { MyItem, MySelect } from './Select'; import {MyColorField} from './ColorField'; function Example() { let [space, setSpace] = React.useState< ColorSpace >('rgb'); return ( <MyColorPicker label="Color" defaultValue="#f80" > <MyColorArea colorSpace="hsb" xChannel="saturation" yChannel="brightness" /> <MyColorSlider colorSpace="hsb" channel="hue" /> <MySelect aria-label="Color space" selectedKey={space} onSelectionChange={(s) => setSpace( s as ColorSpace )} > <MyItem id="rgb"> RGB </MyItem> <MyItem id="hsl"> HSL </MyItem> <MyItem id="hsb"> HSB </MyItem> </MySelect> <div style={{ display: 'flex', gap: 4, width: 192 }} > {getColorChannels( space ).map( (channel) => ( <MyColorField key={channel} colorSpace={space} channel={channel} style={{ flex: 1 }} /> ) )} </div> </MyColorPicker> ); } Color ### Swatches# This example uses a ColorSwatchPicker to provide color presets for a color picker. import {MyColorSwatchPicker, MyColorSwatchPickerItem} from './ColorSwatchPicker'; <MyColorPicker label="Color" defaultValue="#A00"> <MyColorArea colorSpace="hsb" xChannel="saturation" yChannel="brightness" /> <MyColorSlider colorSpace="hsb" channel="hue" /> <MyColorSwatchPicker> <MyColorSwatchPickerItem color="#A00" /> <MyColorSwatchPickerItem color="#f80" /> <MyColorSwatchPickerItem color="#080" /> <MyColorSwatchPickerItem color="#08f" /> <MyColorSwatchPickerItem color="#008" /> </MyColorSwatchPicker> </MyColorPicker> import { MyColorSwatchPicker, MyColorSwatchPickerItem } from './ColorSwatchPicker'; <MyColorPicker label="Color" defaultValue="#A00"> <MyColorArea colorSpace="hsb" xChannel="saturation" yChannel="brightness" /> <MyColorSlider colorSpace="hsb" channel="hue" /> <MyColorSwatchPicker> <MyColorSwatchPickerItem color="#A00" /> <MyColorSwatchPickerItem color="#f80" /> <MyColorSwatchPickerItem color="#080" /> <MyColorSwatchPickerItem color="#08f" /> <MyColorSwatchPickerItem color="#008" /> </MyColorSwatchPicker> </MyColorPicker> import { MyColorSwatchPicker, MyColorSwatchPickerItem } from './ColorSwatchPicker'; <MyColorPicker label="Color" defaultValue="#A00" > <MyColorArea colorSpace="hsb" xChannel="saturation" yChannel="brightness" /> <MyColorSlider colorSpace="hsb" channel="hue" /> <MyColorSwatchPicker> <MyColorSwatchPickerItem color="#A00" /> <MyColorSwatchPickerItem color="#f80" /> <MyColorSwatchPickerItem color="#080" /> <MyColorSwatchPickerItem color="#08f" /> <MyColorSwatchPickerItem color="#008" /> </MyColorSwatchPicker> </MyColorPicker> Color ## Props# * * * | Name | Type | Description | | --- | --- | --- | | `value` | `string | Color ` | The current value (controlled). | | `defaultValue` | `string | Color ` | The default value (uncontrolled). | | `children` | `ReactNode | ( (values: T & & { defaultChildren: ReactNode | | undefined } )) => ReactNode` | The children of the component. A function may be provided to alter the children based on component state. | Events | Name | Type | Description | | --- | --- | --- | | `onChange` | `( (value: Color )) => void` | Handler that is called when the value changes. | Layout | Name | Type | Description | | --- | --- | --- | | `slot` | `string | null` | A slot name for the component. Slots allow the component to receive props from a parent component. An explicit `null` value indicates that the local props completely override all props received from a parent. | ## Advanced customization# * * * ### Contexts# All React Aria Components export a corresponding context that can be used to send props to them from a parent element. This enables you to build your own compositional APIs similar to those found in React Aria Components itself. You can send any prop or ref via context that you could pass to the corresponding component. The local props and ref on the component are merged with the ones passed via context, with the local props taking precedence (following the rules documented in mergeProps). | Component | Context | Props | Ref | | --- | --- | --- | --- | | `ColorPicker` | `ColorPickerContext` | ` ColorPickerProps ` | – | ### State# ColorPicker provides a `ColorPickerState` object to its children via `ColorPickerStateContext`. This can be used to access and manipulate the color area's state. This example uses the browser EyeDropper API (currently available in Chromium-based browsers) to allow users to sample on-screen colors. The ColorPicker is updated when the user chooses a color via the `ColorPickerStateContext`. import {ColorPickerStateContext, parseColor} from 'react-aria-components'; import SamplerIcon from '@spectrum-icons/workflow/Sampler'; function EyeDropperButton() { let state = React.useContext(ColorPickerStateContext)!; // Check browser support. // @ts-ignore if (typeof EyeDropper === 'undefined') { return 'EyeDropper is not supported in your browser.'; } return ( <Button aria-label="Eye dropper" style={{ alignSelf: 'start' }} onPress={() => { // @ts-ignore new EyeDropper().open().then((result) => state.setColor(parseColor(result.sRGBHex)) ); }} > <SamplerIcon size="S" /> </Button> ); } <MyColorPicker label="Color" defaultValue="#345"> <MyColorArea colorSpace="hsb" xChannel="saturation" yChannel="brightness" /> <MyColorSlider colorSpace="hsb" channel="hue" /> <EyeDropperButton /> </MyColorPicker> import { ColorPickerStateContext, parseColor } from 'react-aria-components'; import SamplerIcon from '@spectrum-icons/workflow/Sampler'; function EyeDropperButton() { let state = React.useContext(ColorPickerStateContext)!; // Check browser support. // @ts-ignore if (typeof EyeDropper === 'undefined') { return 'EyeDropper is not supported in your browser.'; } return ( <Button aria-label="Eye dropper" style={{ alignSelf: 'start' }} onPress={() => { // @ts-ignore new EyeDropper().open().then((result) => state.setColor(parseColor(result.sRGBHex)) ); }} > <SamplerIcon size="S" /> </Button> ); } <MyColorPicker label="Color" defaultValue="#345"> <MyColorArea colorSpace="hsb" xChannel="saturation" yChannel="brightness" /> <MyColorSlider colorSpace="hsb" channel="hue" /> <EyeDropperButton /> </MyColorPicker> import { ColorPickerStateContext, parseColor } from 'react-aria-components'; import SamplerIcon from '@spectrum-icons/workflow/Sampler'; function EyeDropperButton() { let state = React .useContext( ColorPickerStateContext )!; // Check browser support. // @ts-ignore if ( typeof EyeDropper === 'undefined' ) { return 'EyeDropper is not supported in your browser.'; } return ( <Button aria-label="Eye dropper" style={{ alignSelf: 'start' }} onPress={() => { // @ts-ignore new EyeDropper() .open().then( (result) => state .setColor( parseColor( result .sRGBHex ) ) ); }} > <SamplerIcon size="S" /> </Button> ); } <MyColorPicker label="Color" defaultValue="#345" > <MyColorArea colorSpace="hsb" xChannel="saturation" yChannel="brightness" /> <MyColorSlider colorSpace="hsb" channel="hue" /> <EyeDropperButton /> </MyColorPicker> Color --- ## Page: https://react-spectrum.adobe.com/react-aria/ColorSlider.html # ColorSlider A color slider allows users to adjust an individual channel of a color value. <table><tbody><tr><th>install</th><td><code>yarn add react-aria-components</code></td></tr><tr><th>version</th><td>1.10.1</td></tr><tr><th>usage</th><td><code><span>import</span> {ColorSlider} <span>from</span> <span>'react-aria-components'</span></code></td></tr></tbody></table> View ARIA pattern W3C View repository GitHub View package NPM ## Example# * * * import {ColorSlider, ColorThumb, Label, SliderOutput, SliderTrack} from 'react-aria-components'; <ColorSlider channel="hue" defaultValue="hsl(0, 100%, 50%)"> <Label /> <SliderOutput /> <SliderTrack> <ColorThumb /> </SliderTrack> </ColorSlider> import { ColorSlider, ColorThumb, Label, SliderOutput, SliderTrack } from 'react-aria-components'; <ColorSlider channel="hue" defaultValue="hsl(0, 100%, 50%)" > <Label /> <SliderOutput /> <SliderTrack> <ColorThumb /> </SliderTrack> </ColorSlider> import { ColorSlider, ColorThumb, Label, SliderOutput, SliderTrack } from 'react-aria-components'; <ColorSlider channel="hue" defaultValue="hsl(0, 100%, 50%)" > <Label /> <SliderOutput /> <SliderTrack> <ColorThumb /> </SliderTrack> </ColorSlider> Hue 0° Show CSS .react-aria-ColorSlider { display: grid; grid-template-areas: "label output" "track track"; grid-template-columns: 1fr auto; gap: 4px; max-width: 300px; .react-aria-Label { grid-area: label; } .react-aria-SliderOutput { grid-area: output; } .react-aria-SliderTrack { grid-area: track; border-radius: 4px; } &[data-orientation=horizontal] { .react-aria-SliderTrack { height: 28px; } .react-aria-ColorThumb { top: 50%; } } } .react-aria-ColorThumb { border: 2px solid white; box-shadow: 0 0 0 1px black, inset 0 0 0 1px black; width: 20px; height: 20px; border-radius: 50%; box-sizing: border-box; &[data-focus-visible] { width: 24px; height: 24px; } } .react-aria-ColorSlider { display: grid; grid-template-areas: "label output" "track track"; grid-template-columns: 1fr auto; gap: 4px; max-width: 300px; .react-aria-Label { grid-area: label; } .react-aria-SliderOutput { grid-area: output; } .react-aria-SliderTrack { grid-area: track; border-radius: 4px; } &[data-orientation=horizontal] { .react-aria-SliderTrack { height: 28px; } .react-aria-ColorThumb { top: 50%; } } } .react-aria-ColorThumb { border: 2px solid white; box-shadow: 0 0 0 1px black, inset 0 0 0 1px black; width: 20px; height: 20px; border-radius: 50%; box-sizing: border-box; &[data-focus-visible] { width: 24px; height: 24px; } } .react-aria-ColorSlider { display: grid; grid-template-areas: "label output" "track track"; grid-template-columns: 1fr auto; gap: 4px; max-width: 300px; .react-aria-Label { grid-area: label; } .react-aria-SliderOutput { grid-area: output; } .react-aria-SliderTrack { grid-area: track; border-radius: 4px; } &[data-orientation=horizontal] { .react-aria-SliderTrack { height: 28px; } .react-aria-ColorThumb { top: 50%; } } } .react-aria-ColorThumb { border: 2px solid white; box-shadow: 0 0 0 1px black, inset 0 0 0 1px black; width: 20px; height: 20px; border-radius: 50%; box-sizing: border-box; &[data-focus-visible] { width: 24px; height: 24px; } } ## Features# * * * The <input type="color"> HTML element can be used to build a color picker, however it is very inconsistent across browsers and operating systems and consists of a complete color picker rather than a single color channel slider. `ColorSlider` helps achieve accessible and touch-friendly color sliders that can be styled as needed. * **Customizable** – Support for adjusting a single channel of RGBA, HSLA, and HSBA colors, in both horizontal and vertical orientations. * **High quality interactions** – Mouse, touch, and keyboard input is supported via the useMove hook. Pressing the track moves the thumb to that position. Text selection and touch scrolling are prevented while dragging. * **Accessible** – Announces localized color descriptions for screen reader users (e.g. "dark vibrant blue"). Uses a visually hidden `<input>` element for mobile screen reader support and HTML form integration. * **International** – Channel value is formatted according to the user's locale. The color slider automatically mirrors all interactions in right-to-left languages. ## Anatomy# * * * A color slider consists of a track element and a thumb that the user can drag to change a single channel of a color value. It may also include optional label and `<output>` elements to display the color channel name and current numeric value, respectively. A visually hidden `<input>` element is used to represent the value to assistive technologies. import {ColorSlider, ColorThumb, Label, SliderOutput, SliderTrack} from 'react-aria-components'; <ColorSlider> <Label /> <SliderOutput /> <SliderTrack> <ColorThumb /> </SliderTrack> </ColorSlider> import { ColorSlider, ColorThumb, Label, SliderOutput, SliderTrack } from 'react-aria-components'; <ColorSlider> <Label /> <SliderOutput /> <SliderTrack> <ColorThumb /> </SliderTrack> </ColorSlider> import { ColorSlider, ColorThumb, Label, SliderOutput, SliderTrack } from 'react-aria-components'; <ColorSlider> <Label /> <SliderOutput /> <SliderTrack> <ColorThumb /> </SliderTrack> </ColorSlider> ### Composed components# A `ColorSlider` uses the following components, which may also be used standalone or reused in other components. Label A label provides context for an input element. ## Starter kits# * * * To help kick-start your project, we offer starter kits that include example implementations of all React Aria components with various styling solutions. All components are fully styled, including support for dark mode, high contrast mode, and all UI states. Each starter comes with a pre-configured Storybook that you can experiment with, or use as a starting point for your own component library. Vanilla CSS Download ZIP Preview Tailwind CSS Download ZIP Preview ## Reusable wrappers# * * * If you will use a ColorSlider in multiple places in your app, you can wrap all of the pieces into a reusable component. This way, the DOM structure, styling code, and other logic are defined in a single place and reused everywhere to ensure consistency. This example wraps `ColorSlider` and all of its children together into a single component which accepts an optional `label` prop, which is passed to the right place. It also shows how to use the `defaultStyle` render prop to add a checkerboard pattern behind partially transparent gradients. import type {ColorSliderProps} from 'react-aria-components'; interface MyColorSliderProps extends ColorSliderProps { label?: string; } export function MyColorSlider({ label, ...props }: MyColorSliderProps) { return ( <ColorSlider {...props}> <Label>{label}</Label> <SliderOutput /> <SliderTrack style={({ defaultStyle }) => ({ background: `${defaultStyle.background}, repeating-conic-gradient(#CCC 0% 25%, white 0% 50%) 50% / 16px 16px` })} > <ColorThumb /> </SliderTrack> </ColorSlider> ); } <MyColorSlider label="Red Opacity" defaultValue="#f00" channel="alpha" /> import type {ColorSliderProps} from 'react-aria-components'; interface MyColorSliderProps extends ColorSliderProps { label?: string; } export function MyColorSlider( { label, ...props }: MyColorSliderProps ) { return ( <ColorSlider {...props}> <Label>{label}</Label> <SliderOutput /> <SliderTrack style={({ defaultStyle }) => ({ background: `${defaultStyle.background}, repeating-conic-gradient(#CCC 0% 25%, white 0% 50%) 50% / 16px 16px` })} > <ColorThumb /> </SliderTrack> </ColorSlider> ); } <MyColorSlider label="Red Opacity" defaultValue="#f00" channel="alpha" /> import type {ColorSliderProps} from 'react-aria-components'; interface MyColorSliderProps extends ColorSliderProps { label?: string; } export function MyColorSlider( { label, ...props }: MyColorSliderProps ) { return ( <ColorSlider {...props} > <Label> {label} </Label> <SliderOutput /> <SliderTrack style={( { defaultStyle } ) => ({ background: `${defaultStyle.background}, repeating-conic-gradient(#CCC 0% 25%, white 0% 50%) 50% / 16px 16px` })} > <ColorThumb /> </SliderTrack> </ColorSlider> ); } <MyColorSlider label="Red Opacity" defaultValue="#f00" channel="alpha" /> Red Opacity 100% ## Value# * * * A ColorSlider requires either an uncontrolled default value or a controlled value, provided using the `defaultValue` or `value` props respectively. The value provided to either of these props should be a color string or `Color` object. The `channel` prop must also be provided to specify which color channel the slider should display. This must be one of the channels included in the color value, for example, for RGB colors, the "red", "green", and "blue" channels are available. For a full list of supported channels, see the Props table below. ### Controlled# In the example below, the `parseColor` function is used to parse the initial color from an HSL string. This is passed to the `value` prop to make the `ColorSlider` controlled, and updated in the `onChange` event. import {parseColor} from 'react-aria-components'; function Example() { let [value, setValue] = React.useState(parseColor('hsl(0, 100%, 50%)')); return ( <> <MyColorSlider label="Hue (controlled)" value={value} onChange={setValue} channel="hue" /> <p>Value: {value.toString('hex')}</p> </> ); } import {parseColor} from 'react-aria-components'; function Example() { let [value, setValue] = React.useState( parseColor('hsl(0, 100%, 50%)') ); return ( <> <MyColorSlider label="Hue (controlled)" value={value} onChange={setValue} channel="hue" /> <p>Value: {value.toString('hex')}</p> </> ); } import {parseColor} from 'react-aria-components'; function Example() { let [value, setValue] = React.useState( parseColor( 'hsl(0, 100%, 50%)' ) ); return ( <> <MyColorSlider label="Hue (controlled)" value={value} onChange={setValue} channel="hue" /> <p> Value:{' '} {value.toString( 'hex' )} </p> </> ); } Hue (controlled) 0° Value: #FF0000 ### HTML forms# ColorSlider supports the `name` prop for integration with HTML forms. The value will be submitted as a number between the minimum and maximum value for the displayed channel. <MyColorSlider defaultValue="#7f0000" channel="red" name="red" /> <MyColorSlider defaultValue="#7f0000" channel="red" name="red" /> <MyColorSlider defaultValue="#7f0000" channel="red" name="red" /> Red 127 ## Events# * * * ColorSlider supports two events: `onChange` and `onChangeEnd`. `onChange` is triggered whenever the ColorSlider's handle is dragged, and `onChangeEnd` is triggered when the user stops dragging the handle. Both events receive a `Color` object as a parameter. The example below uses `onChange` and `onChangeEnd` to update two separate elements with the ColorSlider's value. function Example() { let [currentValue, setCurrentValue] = React.useState( parseColor('hsl(50, 100%, 50%)') ); let [finalValue, setFinalValue] = React.useState(currentValue); return ( <div> <MyColorSlider value={currentValue} channel="hue" onChange={setCurrentValue} onChangeEnd={setFinalValue} /> <p>Current value: {currentValue.toString('hsl')}</p> <p>Final value: {finalValue.toString('hsl')}</p> </div> ); } function Example() { let [currentValue, setCurrentValue] = React.useState( parseColor('hsl(50, 100%, 50%)') ); let [finalValue, setFinalValue] = React.useState( currentValue ); return ( <div> <MyColorSlider value={currentValue} channel="hue" onChange={setCurrentValue} onChangeEnd={setFinalValue} /> <p>Current value: {currentValue.toString('hsl')}</p> <p>Final value: {finalValue.toString('hsl')}</p> </div> ); } function Example() { let [ currentValue, setCurrentValue ] = React.useState( parseColor( 'hsl(50, 100%, 50%)' ) ); let [ finalValue, setFinalValue ] = React.useState( currentValue ); return ( <div> <MyColorSlider value={currentValue} channel="hue" onChange={setCurrentValue} onChangeEnd={setFinalValue} /> <p> Current value: {' '} {currentValue .toString( 'hsl' )} </p> <p> Final value:{' '} {finalValue .toString( 'hsl' )} </p> </div> ); } Hue 50° Current value: hsl(50, 100%, 50%) Final value: hsl(50, 100%, 50%) ## Creating a color picker# * * * ### RGBA# This example shows how you could build an RGBA color picker using four color sliders bound to the same color value in state. The `parseColor` function is used to parse the initial color from a hex value, stored in state. The `value` and `onChange` props of ColorSlider are used to make the sliders controlled, so that they all update when the color is modified. function Example() { let [color, setColor] = React.useState(parseColor('#ff00ff')); return ( <> <MyColorSlider channel="red" value={color} onChange={setColor} /> <MyColorSlider channel="green" value={color} onChange={setColor} /> <MyColorSlider channel="blue" value={color} onChange={setColor} /> <MyColorSlider channel="alpha" value={color} onChange={setColor} /> </> ); } function Example() { let [color, setColor] = React.useState( parseColor('#ff00ff') ); return ( <> <MyColorSlider channel="red" value={color} onChange={setColor} /> <MyColorSlider channel="green" value={color} onChange={setColor} /> <MyColorSlider channel="blue" value={color} onChange={setColor} /> <MyColorSlider channel="alpha" value={color} onChange={setColor} /> </> ); } function Example() { let [color, setColor] = React.useState( parseColor( '#ff00ff' ) ); return ( <> <MyColorSlider channel="red" value={color} onChange={setColor} /> <MyColorSlider channel="green" value={color} onChange={setColor} /> <MyColorSlider channel="blue" value={color} onChange={setColor} /> <MyColorSlider channel="alpha" value={color} onChange={setColor} /> </> ); } Red 255 Green 0 Blue 255 Alpha 100% ### HSLA# This example shows how to build a similar color picker to the one above, using HSLA colors instead. function Example() { let [color, setColor] = React.useState(parseColor('hsla(0, 100%, 50%, 0.5)')); return ( <> <MyColorSlider channel="hue" value={color} onChange={setColor} /> <MyColorSlider channel="saturation" value={color} onChange={setColor} /> <MyColorSlider channel="lightness" value={color} onChange={setColor} /> <MyColorSlider channel="alpha" value={color} onChange={setColor} /> </> ); } function Example() { let [color, setColor] = React.useState( parseColor('hsla(0, 100%, 50%, 0.5)') ); return ( <> <MyColorSlider channel="hue" value={color} onChange={setColor} /> <MyColorSlider channel="saturation" value={color} onChange={setColor} /> <MyColorSlider channel="lightness" value={color} onChange={setColor} /> <MyColorSlider channel="alpha" value={color} onChange={setColor} /> </> ); } function Example() { let [color, setColor] = React.useState( parseColor( 'hsla(0, 100%, 50%, 0.5)' ) ); return ( <> <MyColorSlider channel="hue" value={color} onChange={setColor} /> <MyColorSlider channel="saturation" value={color} onChange={setColor} /> <MyColorSlider channel="lightness" value={color} onChange={setColor} /> <MyColorSlider channel="alpha" value={color} onChange={setColor} /> </> ); } Hue 0° Saturation 100% Lightness 50% Alpha 50% ### HSBA# This example shows how to build an HSBA color picker. function Example() { let [color, setColor] = React.useState(parseColor('hsba(0, 100%, 50%, 0.5)')); return ( <> <MyColorSlider channel="hue" value={color} onChange={setColor} /> <MyColorSlider channel="saturation" value={color} onChange={setColor} /> <MyColorSlider channel="brightness" value={color} onChange={setColor} /> <MyColorSlider channel="alpha" value={color} onChange={setColor} /> </> ); } function Example() { let [color, setColor] = React.useState( parseColor('hsba(0, 100%, 50%, 0.5)') ); return ( <> <MyColorSlider channel="hue" value={color} onChange={setColor} /> <MyColorSlider channel="saturation" value={color} onChange={setColor} /> <MyColorSlider channel="brightness" value={color} onChange={setColor} /> <MyColorSlider channel="alpha" value={color} onChange={setColor} /> </> ); } function Example() { let [color, setColor] = React.useState( parseColor( 'hsba(0, 100%, 50%, 0.5)' ) ); return ( <> <MyColorSlider channel="hue" value={color} onChange={setColor} /> <MyColorSlider channel="saturation" value={color} onChange={setColor} /> <MyColorSlider channel="brightness" value={color} onChange={setColor} /> <MyColorSlider channel="alpha" value={color} onChange={setColor} /> </> ); } Hue 0° Saturation 100% Brightness 50% Alpha 50% ## Visual options# * * * ### Vertical orientation# Sliders are horizontally oriented by default. The `orientation` prop can be set to `"vertical"` to create a vertical slider. This example also hides the visual label. By default, an `aria-label` is provided using the localized channel name (e.g. Hue). <MyColorSlider orientation="vertical" defaultValue="hsb(0, 100%, 100%)" channel="hue" /> <MyColorSlider orientation="vertical" defaultValue="hsb(0, 100%, 100%)" channel="hue" /> <MyColorSlider orientation="vertical" defaultValue="hsb(0, 100%, 100%)" channel="hue" /> Hue 0° Show CSS .react-aria-ColorSlider { &[data-orientation=vertical] { height: 150px; display: block; .react-aria-Label, .react-aria-SliderOutput { display: none; } .react-aria-SliderTrack { width: 28px; height: 100%; } .react-aria-ColorThumb { left: 50%; } } } .react-aria-ColorSlider { &[data-orientation=vertical] { height: 150px; display: block; .react-aria-Label, .react-aria-SliderOutput { display: none; } .react-aria-SliderTrack { width: 28px; height: 100%; } .react-aria-ColorThumb { left: 50%; } } } .react-aria-ColorSlider { &[data-orientation=vertical] { height: 150px; display: block; .react-aria-Label, .react-aria-SliderOutput { display: none; } .react-aria-SliderTrack { width: 28px; height: 100%; } .react-aria-ColorThumb { left: 50%; } } } ### Disabled# A `ColorSlider` can be disabled using the `isDisabled` prop. This prevents the thumb from being focused or dragged. It's up to you to style your color slider to appear disabled accordingly. <MyColorSlider channel="red" defaultValue="#7f007f" isDisabled /> <MyColorSlider channel="red" defaultValue="#7f007f" isDisabled /> <MyColorSlider channel="red" defaultValue="#7f007f" isDisabled /> Red 127 Show CSS .react-aria-ColorSlider { &[data-disabled] { .react-aria-SliderTrack { background: gray !important; } .react-aria-ColorThumb { background: gray !important; opacity: 0.5; } } } .react-aria-ColorSlider { &[data-disabled] { .react-aria-SliderTrack { background: gray !important; } .react-aria-ColorThumb { background: gray !important; opacity: 0.5; } } } .react-aria-ColorSlider { &[data-disabled] { .react-aria-SliderTrack { background: gray !important; } .react-aria-ColorThumb { background: gray !important; opacity: 0.5; } } } ## Labeling# * * * By default, `ColorSlider` provides an `aria-label` for the localized color channel name. If a `<Label>` element is rendered, its children default to the channel name. If you wish to override this with a more specific label, custom children can be provided to the `<Label>`, or an `aria-label` or `aria-labelledby` prop may be passed instead. <ColorSlider channel="hue" defaultValue="hsl(0, 100%, 50%)"> <Label>Background Hue</Label> <SliderOutput /> <SliderTrack> <ColorThumb /> </SliderTrack> </ColorSlider> <ColorSlider aria-label="Background Saturation" channel="saturation" defaultValue="hsl(0, 100%, 50%)"> <SliderTrack> <ColorThumb /> </SliderTrack> </ColorSlider> <ColorSlider channel="hue" defaultValue="hsl(0, 100%, 50%)"> <Label>Background Hue</Label> <SliderOutput /> <SliderTrack> <ColorThumb /> </SliderTrack> </ColorSlider> <ColorSlider aria-label="Background Saturation" channel="saturation" defaultValue="hsl(0, 100%, 50%)"> <SliderTrack> <ColorThumb /> </SliderTrack> </ColorSlider> <ColorSlider channel="hue" defaultValue="hsl(0, 100%, 50%)" > <Label> Background Hue </Label> <SliderOutput /> <SliderTrack> <ColorThumb /> </SliderTrack> </ColorSlider> <ColorSlider aria-label="Background Saturation" channel="saturation" defaultValue="hsl(0, 100%, 50%)" > <SliderTrack> <ColorThumb /> </SliderTrack> </ColorSlider> Background Hue 0° ### Accessibility# The `aria-valuetext` of the `<input>` element is formatted according to the user's locale automatically. It also includes a localized description of the selected color (e.g. "dark vibrant blue"). ### Internationalization# In right-to-left languages, color sliders should be mirrored. The label should be right aligned, and the value should be left aligned. Orientation of the gradient background, positioning of the thumb, and dragging behavior is automatically mirrored by `ColorSlider`. ## Props# * * * ### ColorSlider# | Name | Type | Default | Description | | --- | --- | --- | --- | | `channel` | ` ColorChannel ` | — | The color channel that the slider manipulates. | | `colorSpace` | ` ColorSpace ` | — | The color space that the slider operates in. The `channel` must be in this color space. If not provided, this defaults to the color space of the `color` or `defaultColor` value. | | `orientation` | ` Orientation ` | `'horizontal'` | The orientation of the Slider. | | `isDisabled` | `boolean` | — | Whether the whole Slider is disabled. | | `value` | `T` | — | The current value (controlled). | | `defaultValue` | `T` | — | The default value (uncontrolled). | | `name` | `string` | — | The name of the input element, used when submitting an HTML form. See MDN. | | `children` | `ReactNode | ( (values: ColorSliderRenderProps & & { defaultChildren: ReactNode | | undefined } )) => ReactNode` | — | The children of the component. A function may be provided to alter the children based on component state. | | `className` | `string | ( (values: ColorSliderRenderProps & & { defaultClassName: string | | undefined } )) => string` | — | The CSS className for the element. A function may be provided to compute the class based on component state. | | `style` | `CSSProperties | ( (values: ColorSliderRenderProps & & { defaultStyle: CSSProperties } )) => CSSProperties | undefined` | — | The inline style for the element. A function may be provided to compute the style based on component state. | Events | Name | Type | Description | | --- | --- | --- | | `onChange` | `( (value: Color )) => void` | Handler that is called when the value changes, as the user drags. | | `onChangeEnd` | `( (value: Color )) => void` | Handler that is called when the user stops dragging. | Layout | Name | Type | Description | | --- | --- | --- | | `slot` | `string | null` | A slot name for the component. Slots allow the component to receive props from a parent component. An explicit `null` value indicates that the local props completely override all props received from a parent. | Accessibility | Name | Type | Description | | --- | --- | --- | | `id` | `string` | The element's unique identifier. See MDN. | | `aria-label` | `string` | Defines a string value that labels the current element. | | `aria-labelledby` | `string` | Identifies the element (or elements) that labels the current element. | | `aria-describedby` | `string` | Identifies the element (or elements) that describes the object. | | `aria-details` | `string` | Identifies the element (or elements) that provide a detailed, extended description for the object. | ### Label# A `<Label>` accepts all HTML attributes. ### SliderOutput# A `<SliderOutput>` renders the current value of the color slider as text. Show props | Name | Type | Description | | --- | --- | --- | | `children` | `ReactNode | ( (values: SliderRenderProps & & { defaultChildren: ReactNode | | undefined } )) => ReactNode` | The children of the component. A function may be provided to alter the children based on component state. | | `className` | `string | ( (values: SliderRenderProps & & { defaultClassName: string | | undefined } )) => string` | The CSS className for the element. A function may be provided to compute the class based on component state. | | `style` | `CSSProperties | ( (values: SliderRenderProps & & { defaultStyle: CSSProperties } )) => CSSProperties | undefined` | The inline style for the element. A function may be provided to compute the style based on component state. | ### SliderTrack# The `<SliderTrack>` component renders a gradient representing the colors that can be selected for the color channel, and contains a `<ColorThumb>` element. Show props | Name | Type | Description | | --- | --- | --- | | `children` | `ReactNode | ( (values: SliderTrackRenderProps & & { defaultChildren: ReactNode | | undefined } )) => ReactNode` | The children of the component. A function may be provided to alter the children based on component state. | | `className` | `string | ( (values: SliderTrackRenderProps & & { defaultClassName: string | | undefined } )) => string` | The CSS className for the element. A function may be provided to compute the class based on component state. | | `style` | `CSSProperties | ( (values: SliderTrackRenderProps & & { defaultStyle: CSSProperties } )) => CSSProperties | undefined` | The inline style for the element. A function may be provided to compute the style based on component state. | Events | Name | Type | Description | | --- | --- | --- | | `onHoverStart` | `( (e: HoverEvent )) => void` | Handler that is called when a hover interaction starts. | | `onHoverEnd` | `( (e: HoverEvent )) => void` | Handler that is called when a hover interaction ends. | | `onHoverChange` | `( (isHovering: boolean )) => void` | Handler that is called when the hover state changes. | ### ColorThumb# The `<ColorThumb>` component renders a draggable thumb with a preview of the selected color. Show props | Name | Type | Description | | --- | --- | --- | | `children` | `ReactNode | ( (values: ColorThumbRenderProps & & { defaultChildren: ReactNode | | undefined } )) => ReactNode` | The children of the component. A function may be provided to alter the children based on component state. | | `className` | `string | ( (values: ColorThumbRenderProps & & { defaultClassName: string | | undefined } )) => string` | The CSS className for the element. A function may be provided to compute the class based on component state. | | `style` | `CSSProperties | ( (values: ColorThumbRenderProps & & { defaultStyle: CSSProperties } )) => CSSProperties | undefined` | The inline style for the element. A function may be provided to compute the style based on component state. | Events | Name | Type | Description | | --- | --- | --- | | `onHoverStart` | `( (e: HoverEvent )) => void` | Handler that is called when a hover interaction starts. | | `onHoverEnd` | `( (e: HoverEvent )) => void` | Handler that is called when a hover interaction ends. | | `onHoverChange` | `( (isHovering: boolean )) => void` | Handler that is called when the hover state changes. | ## Styling# * * * React Aria components can be styled in many ways, including using CSS classes, inline styles, utility classes (e.g. Tailwind), CSS-in-JS (e.g. Styled Components), etc. By default, all components include a builtin `className` attribute which can be targeted using CSS selectors. These follow the `react-aria-ComponentName` naming convention. .react-aria-ColorSlider { /* ... */ } .react-aria-ColorSlider { /* ... */ } .react-aria-ColorSlider { /* ... */ } A custom `className` can also be specified on any component. This overrides the default `className` provided by React Aria with your own. <ColorSlider className="my-color-slider"> {/* ... */} </ColorSlider> <ColorSlider className="my-color-slider"> {/* ... */} </ColorSlider> <ColorSlider className="my-color-slider"> {/* ... */} </ColorSlider> In addition, some components support multiple UI states (e.g. focused, placeholder, readonly, etc.). React Aria components expose states using data attributes, which you can target in CSS selectors. For example: .react-aria-ColorThumb[data-dragging] { /* ... */ } .react-aria-ColorThumb[data-focused] { /* ... */ } .react-aria-ColorThumb[data-dragging] { /* ... */ } .react-aria-ColorThumb[data-focused] { /* ... */ } .react-aria-ColorThumb[data-dragging] { /* ... */ } .react-aria-ColorThumb[data-focused] { /* ... */ } The `className` and `style` props also accept functions which receive states for styling. This lets you dynamically determine the classes or styles to apply, which is useful when using utility CSS libraries like Tailwind. <ColorThumb className={({ isDragging }) => isDragging ? 'scale-150' : 'scale-100'} /> <ColorThumb className={({ isDragging }) => isDragging ? 'scale-150' : 'scale-100'} /> <ColorThumb className={( { isDragging } ) => isDragging ? 'scale-150' : 'scale-100'} /> Render props may also be used as children to alter what elements are rendered based on the current state. For example, you could implement custom formatting for the slider's current value. <SliderOutput> {state => `Value: ${state.getThumbValueLabel(0)}`} </SliderOutput> <SliderOutput> {state => `Value: ${state.getThumbValueLabel(0)}`} </SliderOutput> <SliderOutput> {(state) => `Value: ${ state .getThumbValueLabel( 0 ) }`} </SliderOutput> The states, selectors, and render props for each component used in a `ColorSlider` are documented below. ### ColorSlider# The `ColorSlider` component can be targeted with the `.react-aria-ColorSlider` CSS selector, or by overriding with a custom `className`. It supports the following states: | Name | CSS Selector | Description | | --- | --- | --- | | `orientation` | `[data-orientation="horizontal | vertical"]` | The orientation of the color slider. | | `isDisabled` | `[data-disabled]` | Whether the color slider is disabled. | | `state` | `—` | State of the color slider. | ### Label# A `Label` can be targeted with the `.react-aria-Label` CSS selector, or by overriding with a custom `className`. ### SliderOutput# The `SliderOutput` component can be targeted with the `.react-aria-SliderOutput` CSS selector, or by overriding with a custom `className`. It supports the following states: | Name | CSS Selector | Description | | --- | --- | --- | | `orientation` | `[data-orientation="horizontal | vertical"]` | The orientation of the slider. | | `isDisabled` | `[data-disabled]` | Whether the slider is disabled. | | `state` | `—` | State of the slider. | ### SliderTrack# The `SliderTrack` component can be targeted with the `.react-aria-SliderTrack` CSS selector, or by overriding with a custom `className`. It supports the following states: | Name | CSS Selector | Description | | --- | --- | --- | | `isHovered` | `[data-hovered]` | Whether the slider track is currently hovered with a mouse. | | `orientation` | `[data-orientation="horizontal | vertical"]` | The orientation of the slider. | | `isDisabled` | `[data-disabled]` | Whether the slider is disabled. | | `state` | `—` | State of the slider. | ### ColorThumb# The `ColorThumb` component can be targeted with the `.react-aria-ColorThumb` CSS selector, or by overriding with a custom `className`. It supports the following states: | Name | CSS Selector | Description | | --- | --- | --- | | `color` | `—` | The selected color, excluding the alpha channel. | | `isDragging` | `[data-dragging]` | Whether this thumb is currently being dragged. | | `isHovered` | `[data-hovered]` | Whether the thumb is currently hovered with a mouse. | | `isFocused` | `[data-focused]` | Whether the thumb is currently focused. | | `isFocusVisible` | `[data-focus-visible]` | Whether the thumb is keyboard focused. | | `isDisabled` | `[data-disabled]` | Whether the thumb is disabled. | ## Advanced customization# * * * ### Composition# If you need to customize one of the components within a `ColorSlider`, such as `Label` or `SliderOutput`, in many cases you can create a wrapper component. This lets you customize the props passed to the component. function MySliderOutput(props) { return <SliderOutput {...props} className="my-slider-output" /> } function MySliderOutput(props) { return ( <SliderOutput {...props} className="my-slider-output" /> ); } function MySliderOutput( props ) { return ( <SliderOutput {...props} className="my-slider-output" /> ); } ### Contexts# All React Aria Components export a corresponding context that can be used to send props to them from a parent element. This enables you to build your own compositional APIs similar to those found in React Aria Components itself. You can send any prop or ref via context that you could pass to the corresponding component. The local props and ref on the component are merged with the ones passed via context, with the local props taking precedence (following the rules documented in mergeProps). | Component | Context | Props | Ref | | --- | --- | --- | --- | | `ColorSlider` | `ColorSliderContext` | ` ColorSliderProps ` | `HTMLDivElement` | This example shows a `ColorSliderDescription` component that accepts a color slider in its children and renders a description element below it. It uses the useId hook to generate a unique id for the description, and associates it with the color slider via the `aria-describedby` attribute passed to the `ColorSliderContext` provider. import {ColorSliderContext} from 'react-aria-components'; import {useId} from 'react-aria'; interface ColorSliderDescriptionProps { children?: React.ReactNode; description?: string; } function ColorSliderDescription( { children, description }: ColorSliderDescriptionProps ) { let descriptionId = useId(); return ( <div> <ColorSliderContext.Provider value={{ 'aria-describedby': descriptionId }} > {children} </ColorSliderContext.Provider> <small id={descriptionId}>{description}</small> </div> ); } <ColorSliderDescription description="It's not easy being green."> <MyColorSlider channel="green" defaultValue="#006" /> </ColorSliderDescription> import {ColorSliderContext} from 'react-aria-components'; import {useId} from 'react-aria'; interface ColorSliderDescriptionProps { children?: React.ReactNode; description?: string; } function ColorSliderDescription( { children, description }: ColorSliderDescriptionProps ) { let descriptionId = useId(); return ( <div> <ColorSliderContext.Provider value={{ 'aria-describedby': descriptionId }} > {children} </ColorSliderContext.Provider> <small id={descriptionId}>{description}</small> </div> ); } <ColorSliderDescription description="It's not easy being green."> <MyColorSlider channel="green" defaultValue="#006" /> </ColorSliderDescription> import {ColorSliderContext} from 'react-aria-components'; import {useId} from 'react-aria'; interface ColorSliderDescriptionProps { children?: React.ReactNode; description?: string; } function ColorSliderDescription( { children, description }: ColorSliderDescriptionProps ) { let descriptionId = useId(); return ( <div> <ColorSliderContext.Provider value={{ 'aria-describedby': descriptionId }} > {children} </ColorSliderContext.Provider> <small id={descriptionId} > {description} </small> </div> ); } <ColorSliderDescription description="It's not easy being green."> <MyColorSlider channel="green" defaultValue="#006" /> </ColorSliderDescription> Green 0 It's not easy being green. ### Custom children# ColorSlider passes props to its child components, such as the label, via their associated contexts. These contexts are exported so you can also consume them in your own custom components. This enables you to reuse existing components from your app or component library together with React Aria Components. | Component | Context | Props | Ref | | --- | --- | --- | --- | | `Label` | `LabelContext` | ` LabelProps ` | `HTMLLabelElement` | This example consumes from `LabelContext` in an existing styled label component to make it compatible with React Aria Components. The `useContextProps` hook merges the local props and ref with the ones provided via context by ColorSlider. import type {LabelProps} from 'react-aria-components'; import {LabelContext, useContextProps} from 'react-aria-components'; const MyCustomLabel = React.forwardRef( (props: LabelProps, ref: React.ForwardedRef<HTMLLabelElement>) => { // Merge the local props and ref with the ones provided via context. [props, ref] = useContextProps(props, ref, LabelContext); // ... your existing Label component return <label {...props} ref={ref} />; } ); import type {LabelProps} from 'react-aria-components'; import { LabelContext, useContextProps } from 'react-aria-components'; const MyCustomLabel = React.forwardRef( ( props: LabelProps, ref: React.ForwardedRef<HTMLLabelElement> ) => { // Merge the local props and ref with the ones provided via context. [props, ref] = useContextProps( props, ref, LabelContext ); // ... your existing Label component return <label {...props} ref={ref} />; } ); import type {LabelProps} from 'react-aria-components'; import { LabelContext, useContextProps } from 'react-aria-components'; const MyCustomLabel = React.forwardRef( ( props: LabelProps, ref: React.ForwardedRef< HTMLLabelElement > ) => { // Merge the local props and ref with the ones provided via context. [props, ref] = useContextProps( props, ref, LabelContext ); // ... your existing Label component return ( <label {...props} ref={ref} /> ); } ); Now you can use `MyCustomLabel` within a `ColorSlider`, in place of the builtin React Aria Components `Label`. <ColorSlider> <MyCustomLabel>Opacity</MyCustomLabel> <SliderTrack> <ColorThumb /> </SliderTrack> </ColorSlider> <ColorSlider> <MyCustomLabel>Opacity</MyCustomLabel> <SliderTrack> <ColorThumb /> </SliderTrack> </ColorSlider> <ColorSlider> <MyCustomLabel> Opacity </MyCustomLabel> <SliderTrack> <ColorThumb /> </SliderTrack> </ColorSlider> ### State# ColorSlider provides a `ColorSliderState` object to its children via `ColorSliderStateContext`. This can be used to access and manipulate the slider's state. This example shows a `ColorNumberField` component that can be placed within a `ColorSlider` to allow the user to enter a number and update the channel value. import {ColorSliderStateContext, Input, LabelContext, NumberField, useSlottedContext} from 'react-aria-components'; function ColorNumberField({ channel }) { let state = React.useContext(ColorSliderStateContext)!; let labelProps = useSlottedContext(LabelContext)!; return ( <NumberField aria-labelledby={labelProps.id} value={state.value.getChannelValue(channel)} minValue={state.value.getChannelRange(channel).minValue} maxValue={state.value.getChannelRange(channel).maxValue} onChange={(v) => state.setValue(state.value.withChannelValue(channel, v))} formatOptions={state.value.getChannelFormatOptions(channel)} > <Input /> </NumberField> ); } <ColorSlider channel="hue" defaultValue="hsl(0, 100%, 50%)"> <Label /> <ColorNumberField channel="hue" /> <SliderTrack> <ColorThumb /> </SliderTrack> </ColorSlider> import { ColorSliderStateContext, Input, LabelContext, NumberField, useSlottedContext } from 'react-aria-components'; function ColorNumberField({ channel }) { let state = React.useContext(ColorSliderStateContext)!; let labelProps = useSlottedContext(LabelContext)!; return ( <NumberField aria-labelledby={labelProps.id} value={state.value.getChannelValue(channel)} minValue={state.value.getChannelRange(channel) .minValue} maxValue={state.value.getChannelRange(channel) .maxValue} onChange={(v) => state.setValue( state.value.withChannelValue(channel, v) )} formatOptions={state.value.getChannelFormatOptions( channel )} > <Input /> </NumberField> ); } <ColorSlider channel="hue" defaultValue="hsl(0, 100%, 50%)" > <Label /> <ColorNumberField channel="hue" /> <SliderTrack> <ColorThumb /> </SliderTrack> </ColorSlider> import { ColorSliderStateContext, Input, LabelContext, NumberField, useSlottedContext } from 'react-aria-components'; function ColorNumberField( { channel } ) { let state = React .useContext( ColorSliderStateContext )!; let labelProps = useSlottedContext( LabelContext )!; return ( <NumberField aria-labelledby={labelProps .id} value={state.value .getChannelValue( channel )} minValue={state .value .getChannelRange( channel ).minValue} maxValue={state .value .getChannelRange( channel ).maxValue} onChange={(v) => state.setValue( state.value .withChannelValue( channel, v ) )} formatOptions={state .value .getChannelFormatOptions( channel )} > <Input /> </NumberField> ); } <ColorSlider channel="hue" defaultValue="hsl(0, 100%, 50%)" > <Label /> <ColorNumberField channel="hue" /> <SliderTrack> <ColorThumb /> </SliderTrack> </ColorSlider> Hue Show CSS .react-aria-Input { width: 4ch; } .react-aria-Input { width: 4ch; } .react-aria-Input { width: 4ch; } ### Hooks# If you need to customize things even further, such as accessing internal state or customizing DOM structure, you can drop down to the lower level Hook-based API. See useColorSlider for more details. --- ## Page: https://react-spectrum.adobe.com/react-aria/ColorSwatch.html # ColorSwatch A ColorSwatch displays a preview of a selected color. <table><tbody><tr><th>install</th><td><code>yarn add react-aria-components</code></td></tr><tr><th>version</th><td>1.10.1</td></tr><tr><th>usage</th><td><code><span>import</span> {ColorSwatch} <span>from</span> <span>'react-aria-components'</span></code></td></tr></tbody></table> View ARIA pattern W3C View repository GitHub View package NPM ## Example# * * * import {ColorSwatch} from 'react-aria-components'; <ColorSwatch color="#f00" /> import {ColorSwatch} from 'react-aria-components'; <ColorSwatch color="#f00" /> import {ColorSwatch} from 'react-aria-components'; <ColorSwatch color="#f00" /> Show CSS .react-aria-ColorSwatch { width: 32px; height: 32px; border-radius: 4px; box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.1); } .react-aria-ColorSwatch { width: 32px; height: 32px; border-radius: 4px; box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.1); } .react-aria-ColorSwatch { width: 32px; height: 32px; border-radius: 4px; box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.1); } ## Features# * * * A color swatch may seem simple to build with a `<div>`, but requires additional semantics and labeling for accessibility. * **Accessible** – Includes a localized color description for screen reader users (e.g. "dark vibrant blue"). Uses the img role with a custom `aria-roledescription` of "color swatch". * **International** – Accessible color description and role description are translated into over 30 languages. ## Anatomy# * * * A color swatch consists of a color preview, which is exposed to assistive technology with a localized color description. import {ColorSwatch} from 'react-aria-components'; <ColorSwatch /> import {ColorSwatch} from 'react-aria-components'; <ColorSwatch /> import {ColorSwatch} from 'react-aria-components'; <ColorSwatch /> ## Starter kits# * * * To help kick-start your project, we offer starter kits that include example implementations of all React Aria components with various styling solutions. All components are fully styled, including support for dark mode, high contrast mode, and all UI states. Each starter comes with a pre-configured Storybook that you can experiment with, or use as a starting point for your own component library. Vanilla CSS Download ZIP Preview Tailwind CSS Download ZIP Preview ## Reusable wrappers# * * * If you will use a ColorSwatch in multiple places in your app, you can wrap all of the pieces into a reusable component. This way, the DOM structure, styling code, and other logic are defined in a single place and reused everywhere to ensure consistency. This example wraps `ColorSwatch` and shows how to use the `color` render prop to add a checkerboard pattern behind partially transparent colors. import type {ColorSwatchProps} from 'react-aria-components'; export function MyColorSwatch(props: ColorSwatchProps) { return ( <ColorSwatch {...props} style={({color}) => ({ background: `linear-gradient(${color}, ${color}), repeating-conic-gradient(#CCC 0% 25%, white 0% 50%) 50% / 16px 16px` })} /> ); } <MyColorSwatch color="#f00a" /> import type {ColorSwatchProps} from 'react-aria-components'; export function MyColorSwatch(props: ColorSwatchProps) { return ( <ColorSwatch {...props} style={({ color }) => ({ background: `linear-gradient(${color}, ${color}), repeating-conic-gradient(#CCC 0% 25%, white 0% 50%) 50% / 16px 16px` })} /> ); } <MyColorSwatch color="#f00a" /> import type {ColorSwatchProps} from 'react-aria-components'; export function MyColorSwatch( props: ColorSwatchProps ) { return ( <ColorSwatch {...props} style={( { color } ) => ({ background: `linear-gradient(${color}, ${color}), repeating-conic-gradient(#CCC 0% 25%, white 0% 50%) 50% / 16px 16px` })} /> ); } <MyColorSwatch color="#f00a" /> ## Value# * * * ColorSwatch accepts a value via the `color` prop. The value should be a color string or `Color` object. In the example below, the `parseColor` function is used to parse the initial color from an HSL string. This is passed to the `value` prop of a ColorSlider, and the `color` prop of a `ColorSwatch` to show a preview of the selected color. import {ColorSlider, ColorThumb, parseColor, SliderTrack} from 'react-aria-components'; function Example() { let [color, setColor] = React.useState(parseColor('hsl(0, 100%, 50%)')); return ( <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}> <ColorSlider value={color} onChange={setColor} channel="hue"> <SliderTrack> <ColorThumb /> </SliderTrack> </ColorSlider> <MyColorSwatch color={color} /> </div> ); } import { ColorSlider, ColorThumb, parseColor, SliderTrack } from 'react-aria-components'; function Example() { let [color, setColor] = React.useState( parseColor('hsl(0, 100%, 50%)') ); return ( <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }} > <ColorSlider value={color} onChange={setColor} channel="hue" > <SliderTrack> <ColorThumb /> </SliderTrack> </ColorSlider> <MyColorSwatch color={color} /> </div> ); } import { ColorSlider, ColorThumb, parseColor, SliderTrack } from 'react-aria-components'; function Example() { let [color, setColor] = React.useState( parseColor( 'hsl(0, 100%, 50%)' ) ); return ( <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }} > <ColorSlider value={color} onChange={setColor} channel="hue" > <SliderTrack> <ColorThumb /> </SliderTrack> </ColorSlider> <MyColorSwatch color={color} /> </div> ); } ## Labeling# * * * By default, ColorSwatch includes a localized color description for screen reader users (e.g. "dark vibrant blue") as an `aria-label`. If you have a more specific color name (e.g. Pantone colors), the automatically generated color description can be overridden via the `colorName` prop. An additional label describing the context of the color swatch (e.g. "Background color") can also be provided via the `aria-label` or `aria-labelledby` props. In the example below, the full accessible name of the color swatch will be "Fire truck red, Background color". <MyColorSwatch color="#f00" aria-label="Background color" colorName="Fire truck red" /> <MyColorSwatch color="#f00" aria-label="Background color" colorName="Fire truck red" /> <MyColorSwatch color="#f00" aria-label="Background color" colorName="Fire truck red" /> ## Props# * * * | Name | Type | Description | | --- | --- | --- | | `color` | `string | Color ` | The color value to display in the swatch. | | `colorName` | `string` | A localized accessible name for the color. By default, a description is generated from the color value, but this can be overridden if you have a more specific color name (e.g. Pantone colors). | | `className` | `string | ( (values: ColorSwatchRenderProps & & { defaultClassName: string | | undefined } )) => string` | The CSS className for the element. A function may be provided to compute the class based on component state. | | `style` | `CSSProperties | ( (values: ColorSwatchRenderProps & & { defaultStyle: CSSProperties } )) => CSSProperties | undefined` | The inline style for the element. A function may be provided to compute the style based on component state. | Layout | Name | Type | Description | | --- | --- | --- | | `slot` | `string | null` | A slot name for the component. Slots allow the component to receive props from a parent component. An explicit `null` value indicates that the local props completely override all props received from a parent. | Accessibility | Name | Type | Description | | --- | --- | --- | | `id` | `string` | The element's unique identifier. See MDN. | | `aria-label` | `string` | Defines a string value that labels the current element. | | `aria-labelledby` | `string` | Identifies the element (or elements) that labels the current element. | | `aria-describedby` | `string` | Identifies the element (or elements) that describes the object. | | `aria-details` | `string` | Identifies the element (or elements) that provide a detailed, extended description for the object. | ## Styling# * * * React Aria components can be styled in many ways, including using CSS classes, inline styles, utility classes (e.g. Tailwind), CSS-in-JS (e.g. Styled Components), etc. By default, all components include a builtin `className` attribute which can be targeted using CSS selectors. These follow the `react-aria-ComponentName` naming convention. .react-aria-ColorSwatch { /* ... */ } .react-aria-ColorSwatch { /* ... */ } .react-aria-ColorSwatch { /* ... */ } A custom `className` can also be specified on any component. This overrides the default `className` provided by React Aria with your own. <ColorSwatch className="my-color-swatch"> {/* ... */} </ColorSwatch> <ColorSwatch className="my-color-swatch"> {/* ... */} </ColorSwatch> <ColorSwatch className="my-color-swatch"> {/* ... */} </ColorSwatch> The `className` and `style` props also accept functions which receive states for styling. This lets you dynamically determine the classes or styles to apply, which is useful when using utility CSS libraries like Tailwind. <ColorSwatch style={({ color }) => ({ background: color.toString('css') })} /> <ColorSwatch style={({ color }) => ({ background: color.toString('css') })} /> <ColorSwatch style={( { color } ) => ({ background: color .toString('css') })} /> The render props for `ColorSwatch` are documented below. | Name | Description | | --- | --- | | `color` | The color of the swatch. | ## Advanced customization# * * * ### Contexts# All React Aria Components export a corresponding context that can be used to send props to them from a parent element. This enables you to build your own compositional APIs similar to those found in React Aria Components itself. You can send any prop or ref via context that you could pass to the corresponding component. The local props and ref on the component are merged with the ones passed via context, with the local props taking precedence (following the rules documented in mergeProps). | Component | Context | Props | Ref | | --- | --- | --- | --- | | `ColorSwatch` | `ColorSwatchContext` | ` ColorSwatchProps ` | `HTMLDivElement` | import {ColorSwatchContext} from 'react-aria-components'; <ColorSwatchContext.Provider value={{color: '#ff0'}}> <ColorSwatch /> </ColorSwatchContext.Provider> import {ColorSwatchContext} from 'react-aria-components'; <ColorSwatchContext.Provider value={{color: '#ff0'}}> <ColorSwatch /> </ColorSwatchContext.Provider> import {ColorSwatchContext} from 'react-aria-components'; <ColorSwatchContext.Provider value={{ color: '#ff0' }} > <ColorSwatch /> </ColorSwatchContext.Provider> ### Hooks# If you need to customize things even further, such as accessing internal state or customizing DOM structure, you can drop down to the lower level Hook-based API. See useColorSwatch for more details. --- ## Page: https://react-spectrum.adobe.com/react-aria/ColorSwatchPicker.html # ColorSwatchPicker A ColorSwatchPicker displays a list of color swatches and allows a user to select one of them. <table><tbody><tr><th>install</th><td><code>yarn add react-aria-components</code></td></tr><tr><th>version</th><td>1.10.1</td></tr><tr><th>usage</th><td><code><span>import</span> {ColorSwatchPicker} <span>from</span> <span>'react-aria-components'</span></code></td></tr></tbody></table> View ARIA pattern W3C View repository GitHub View package NPM ## Example# * * * import {ColorSwatch, ColorSwatchPicker, ColorSwatchPickerItem} from 'react-aria-components'; <ColorSwatchPicker> <ColorSwatchPickerItem color="#A00"> <ColorSwatch /> </ColorSwatchPickerItem> <ColorSwatchPickerItem color="#f80"> <ColorSwatch /> </ColorSwatchPickerItem> <ColorSwatchPickerItem color="#080"> <ColorSwatch /> </ColorSwatchPickerItem> <ColorSwatchPickerItem color="#08f"> <ColorSwatch /> </ColorSwatchPickerItem> <ColorSwatchPickerItem color="#088"> <ColorSwatch /> </ColorSwatchPickerItem> <ColorSwatchPickerItem color="#008"> <ColorSwatch /> </ColorSwatchPickerItem> </ColorSwatchPicker> import { ColorSwatch, ColorSwatchPicker, ColorSwatchPickerItem } from 'react-aria-components'; <ColorSwatchPicker> <ColorSwatchPickerItem color="#A00"> <ColorSwatch /> </ColorSwatchPickerItem> <ColorSwatchPickerItem color="#f80"> <ColorSwatch /> </ColorSwatchPickerItem> <ColorSwatchPickerItem color="#080"> <ColorSwatch /> </ColorSwatchPickerItem> <ColorSwatchPickerItem color="#08f"> <ColorSwatch /> </ColorSwatchPickerItem> <ColorSwatchPickerItem color="#088"> <ColorSwatch /> </ColorSwatchPickerItem> <ColorSwatchPickerItem color="#008"> <ColorSwatch /> </ColorSwatchPickerItem> </ColorSwatchPicker> import { ColorSwatch, ColorSwatchPicker, ColorSwatchPickerItem } from 'react-aria-components'; <ColorSwatchPicker> <ColorSwatchPickerItem color="#A00"> <ColorSwatch /> </ColorSwatchPickerItem> <ColorSwatchPickerItem color="#f80"> <ColorSwatch /> </ColorSwatchPickerItem> <ColorSwatchPickerItem color="#080"> <ColorSwatch /> </ColorSwatchPickerItem> <ColorSwatchPickerItem color="#08f"> <ColorSwatch /> </ColorSwatchPickerItem> <ColorSwatchPickerItem color="#088"> <ColorSwatch /> </ColorSwatchPickerItem> <ColorSwatchPickerItem color="#008"> <ColorSwatch /> </ColorSwatchPickerItem> </ColorSwatchPicker> Show CSS @import "@react-aria/example-theme"; .react-aria-ColorSwatchPicker { display: flex; gap: 8px; flex-wrap: wrap; } .react-aria-ColorSwatchPickerItem { position: relative; outline: none; border-radius: 4px; width: fit-content; forced-color-adjust: none; &[data-focus-visible] { outline: 2px solid var(--focus-ring-color); outline-offset: 2px; } &[data-selected]::after { content: ''; position: absolute; inset: 0; border: 2px solid black; outline: 2px solid white; outline-offset: -4px; border-radius: inherit; } } @import "@react-aria/example-theme"; .react-aria-ColorSwatchPicker { display: flex; gap: 8px; flex-wrap: wrap; } .react-aria-ColorSwatchPickerItem { position: relative; outline: none; border-radius: 4px; width: fit-content; forced-color-adjust: none; &[data-focus-visible] { outline: 2px solid var(--focus-ring-color); outline-offset: 2px; } &[data-selected]::after { content: ''; position: absolute; inset: 0; border: 2px solid black; outline: 2px solid white; outline-offset: -4px; border-radius: inherit; } } @import "@react-aria/example-theme"; .react-aria-ColorSwatchPicker { display: flex; gap: 8px; flex-wrap: wrap; } .react-aria-ColorSwatchPickerItem { position: relative; outline: none; border-radius: 4px; width: fit-content; forced-color-adjust: none; &[data-focus-visible] { outline: 2px solid var(--focus-ring-color); outline-offset: 2px; } &[data-selected]::after { content: ''; position: absolute; inset: 0; border: 2px solid black; outline: 2px solid white; outline-offset: -4px; border-radius: inherit; } } ## Features# * * * A `ColorSwatchPicker` wraps the ListBox component to simplify building color swatch pickers that work with a ColorPicker. * **Item selection** – A single color value can be selected from a list of unique colors. Color swatches are automatically matched with the value no matter the color space they are defined in. * **Keyboard navigation** – Color swatches can be navigated using the arrow keys, along with page up/down, home/end, etc. * **Layout options** – Items can be arranged in a vertical or horizontal stack, or as a two-dimensional grid. * **Accessible** – Follows the ARIA listbox pattern. Each swatch includes a localized color description for screen reader users (e.g. "dark vibrant blue"). * **Styleable** – Items include builtin states for styling, such as hover, press, focus, selected, and disabled. ## Anatomy# * * * A ColorSwatchPicker consists of a container element, with a list of color swatches inside. Users can select a color by clicking, tapping, or navigating with the keyboard. import {ColorSwatch, ColorSwatchPicker, ColorSwatchPickerItem} from 'react-aria-components'; <ColorSwatchPicker> <ColorSwatchPickerItem> <ColorSwatch /> </ColorSwatchPickerItem> </ColorSwatchPicker> import { ColorSwatch, ColorSwatchPicker, ColorSwatchPickerItem } from 'react-aria-components'; <ColorSwatchPicker> <ColorSwatchPickerItem> <ColorSwatch /> </ColorSwatchPickerItem> </ColorSwatchPicker> import { ColorSwatch, ColorSwatchPicker, ColorSwatchPickerItem } from 'react-aria-components'; <ColorSwatchPicker> <ColorSwatchPickerItem> <ColorSwatch /> </ColorSwatchPickerItem> </ColorSwatchPicker> ColorSwatchPicker is a convenience wrapper around the ListBox component. If you need additional flexibility, such as support for multiple selection, duplicate colors, drag and drop, etc., you can use the ListBox component directly. ## Starter kits# * * * To help kick-start your project, we offer starter kits that include example implementations of all React Aria components with various styling solutions. All components are fully styled, including support for dark mode, high contrast mode, and all UI states. Each starter comes with a pre-configured Storybook that you can experiment with, or use as a starting point for your own component library. Vanilla CSS Download ZIP Preview Tailwind CSS Download ZIP Preview ## Reusable wrappers# * * * If you will use a ColorSwatchPicker in multiple places in your app, you can wrap all of the pieces into a reusable component. This way, the DOM structure, styling code, and other logic are defined in a single place and reused everywhere to ensure consistency. This example wraps `ColorSwatchPicker` and `ColorSwatchPickerItem`, reusing the `MyColorSwatch` component from the ColorSwatch docs. import type {ColorSwatchPickerItemProps, ColorSwatchPickerProps} from 'react-aria-components'; import {MyColorSwatch} from './ColorSwatch'; export function MyColorSwatchPicker( { children, ...props }: ColorSwatchPickerProps ) { return ( <ColorSwatchPicker {...props}> {children} </ColorSwatchPicker> ); } export function MyColorSwatchPickerItem(props: ColorSwatchPickerItemProps) { return ( <ColorSwatchPickerItem {...props}> <MyColorSwatch /> </ColorSwatchPickerItem> ); } <MyColorSwatchPicker> <MyColorSwatchPickerItem color="#A00" /> <MyColorSwatchPickerItem color="#f80" /> <MyColorSwatchPickerItem color="#080" /> <MyColorSwatchPickerItem color="#08f" /> <MyColorSwatchPickerItem color="#088" /> <MyColorSwatchPickerItem color="#008" /> </MyColorSwatchPicker> import type { ColorSwatchPickerItemProps, ColorSwatchPickerProps } from 'react-aria-components'; import {MyColorSwatch} from './ColorSwatch'; export function MyColorSwatchPicker( { children, ...props }: ColorSwatchPickerProps ) { return ( <ColorSwatchPicker {...props}> {children} </ColorSwatchPicker> ); } export function MyColorSwatchPickerItem( props: ColorSwatchPickerItemProps ) { return ( <ColorSwatchPickerItem {...props}> <MyColorSwatch /> </ColorSwatchPickerItem> ); } <MyColorSwatchPicker> <MyColorSwatchPickerItem color="#A00" /> <MyColorSwatchPickerItem color="#f80" /> <MyColorSwatchPickerItem color="#080" /> <MyColorSwatchPickerItem color="#08f" /> <MyColorSwatchPickerItem color="#088" /> <MyColorSwatchPickerItem color="#008" /> </MyColorSwatchPicker> import type { ColorSwatchPickerItemProps, ColorSwatchPickerProps } from 'react-aria-components'; import {MyColorSwatch} from './ColorSwatch'; export function MyColorSwatchPicker( { children, ...props }: ColorSwatchPickerProps ) { return ( <ColorSwatchPicker {...props} > {children} </ColorSwatchPicker> ); } export function MyColorSwatchPickerItem( props: ColorSwatchPickerItemProps ) { return ( <ColorSwatchPickerItem {...props} > <MyColorSwatch /> </ColorSwatchPickerItem> ); } <MyColorSwatchPicker> <MyColorSwatchPickerItem color="#A00" /> <MyColorSwatchPickerItem color="#f80" /> <MyColorSwatchPickerItem color="#080" /> <MyColorSwatchPickerItem color="#08f" /> <MyColorSwatchPickerItem color="#088" /> <MyColorSwatchPickerItem color="#008" /> </MyColorSwatchPicker> ## Value# * * * ColorSwatchPicker accepts either an uncontrolled default value or a controlled value, provided using the `defaultValue` or `value` props respectively. The value provided to either of these props should be a color string or `Color` object. The value is matched against the color of each ColorSwatch, including equivalent colors in different color spaces. In the example below, the `parseColor` function is used to parse the initial color from a HSL string so that `value`'s type remains consistent. import {parseColor} from 'react-aria-components'; function Example() { let [color, setColor] = React.useState(parseColor('hsl(0, 100%, 33.33%)')); return ( <MyColorSwatchPicker value={color} onChange={setColor}> <MyColorSwatchPickerItem color="#A00" /> <MyColorSwatchPickerItem color="#f80" /> <MyColorSwatchPickerItem color="#080" /> </MyColorSwatchPicker> ); } import {parseColor} from 'react-aria-components'; function Example() { let [color, setColor] = React.useState( parseColor('hsl(0, 100%, 33.33%)') ); return ( <MyColorSwatchPicker value={color} onChange={setColor}> <MyColorSwatchPickerItem color="#A00" /> <MyColorSwatchPickerItem color="#f80" /> <MyColorSwatchPickerItem color="#080" /> </MyColorSwatchPicker> ); } import {parseColor} from 'react-aria-components'; function Example() { let [color, setColor] = React.useState( parseColor( 'hsl(0, 100%, 33.33%)' ) ); return ( <MyColorSwatchPicker value={color} onChange={setColor} > <MyColorSwatchPickerItem color="#A00" /> <MyColorSwatchPickerItem color="#f80" /> <MyColorSwatchPickerItem color="#080" /> </MyColorSwatchPicker> ); } **Note**: ColorSwatches rendered inside a ColorSwatchPicker must have unique colors, even between different color spaces. For example, the values `#f00`, `hsl(0, 100%, 50%)`, and `hsb(0, 100%, 100%)` are all equivalent and considered duplicates. This is required so that selection behavior works properly. ## Labeling# * * * By default, `ColorSwatchPicker` has an `aria-label` with the localized string "Color swatches". This can be overridden with a more specific accessibility label using the `aria-label` or `aria-labelledby` props. All labels should be localized. <MyColorSwatchPicker aria-label="Fill color"> <MyColorSwatchPickerItem color="#A00" /> <MyColorSwatchPickerItem color="#f80" /> <MyColorSwatchPickerItem color="#080" /> </MyColorSwatchPicker> <MyColorSwatchPicker aria-label="Fill color"> <MyColorSwatchPickerItem color="#A00" /> <MyColorSwatchPickerItem color="#f80" /> <MyColorSwatchPickerItem color="#080" /> </MyColorSwatchPicker> <MyColorSwatchPicker aria-label="Fill color"> <MyColorSwatchPickerItem color="#A00" /> <MyColorSwatchPickerItem color="#f80" /> <MyColorSwatchPickerItem color="#080" /> </MyColorSwatchPicker> ## Events# * * * ColorSwatchPicker accepts an `onChange` prop which is triggered whenever the value is edited by the user. It receives a `Color` object as a parameter. The example below uses `onChange` to update a separate element with the color value as a string. import {parseColor} from 'react-aria-components'; function Example() { let [value, setValue] = React.useState(parseColor('#A00')); return ( <div> <MyColorSwatchPicker value={value} onChange={setValue}> <MyColorSwatchPickerItem color="#A00" /> <MyColorSwatchPickerItem color="#f80" /> <MyColorSwatchPickerItem color="#080" /> <MyColorSwatchPickerItem color="#08f" /> <MyColorSwatchPickerItem color="#088" /> <MyColorSwatchPickerItem color="#008" /> </MyColorSwatchPicker> <p>Selected color: {value.toString('rgb')}</p> </div> ); } import {parseColor} from 'react-aria-components'; function Example() { let [value, setValue] = React.useState( parseColor('#A00') ); return ( <div> <MyColorSwatchPicker value={value} onChange={setValue} > <MyColorSwatchPickerItem color="#A00" /> <MyColorSwatchPickerItem color="#f80" /> <MyColorSwatchPickerItem color="#080" /> <MyColorSwatchPickerItem color="#08f" /> <MyColorSwatchPickerItem color="#088" /> <MyColorSwatchPickerItem color="#008" /> </MyColorSwatchPicker> <p>Selected color: {value.toString('rgb')}</p> </div> ); } import {parseColor} from 'react-aria-components'; function Example() { let [value, setValue] = React.useState( parseColor('#A00') ); return ( <div> <MyColorSwatchPicker value={value} onChange={setValue} > <MyColorSwatchPickerItem color="#A00" /> <MyColorSwatchPickerItem color="#f80" /> <MyColorSwatchPickerItem color="#080" /> <MyColorSwatchPickerItem color="#08f" /> <MyColorSwatchPickerItem color="#088" /> <MyColorSwatchPickerItem color="#008" /> </MyColorSwatchPicker> <p> Selected color: {' '} {value.toString( 'rgb' )} </p> </div> ); } Selected color: rgb(170, 0, 0) ## Disabled# * * * A `ColorSwatchPickerItem` can be disabled using the `isDisabled` prop. Disabled swatches cannot be selected, and are not focusable or interactive. <MyColorSwatchPicker> <MyColorSwatchPickerItem color="#A00" /> <MyColorSwatchPickerItem color="#f80" isDisabled /> <MyColorSwatchPickerItem color="#080" /> </MyColorSwatchPicker> <MyColorSwatchPicker> <MyColorSwatchPickerItem color="#A00" /> <MyColorSwatchPickerItem color="#f80" isDisabled /> <MyColorSwatchPickerItem color="#080" /> </MyColorSwatchPicker> <MyColorSwatchPicker> <MyColorSwatchPickerItem color="#A00" /> <MyColorSwatchPickerItem color="#f80" isDisabled /> <MyColorSwatchPickerItem color="#080" /> </MyColorSwatchPicker> Show CSS .react-aria-ColorSwatchPickerItem { &[data-disabled] { opacity: 0.2; } } .react-aria-ColorSwatchPickerItem { &[data-disabled] { opacity: 0.2; } } .react-aria-ColorSwatchPickerItem { &[data-disabled] { opacity: 0.2; } } ## Stack layout# * * * By default, ColorSwatchPicker expects items to be arranged horizontally, optionally wrapping to form a grid, and implements keyboard navigation and drag and drop accordingly. The `layout` prop can be used to display the swatches as a vertical stack instead. <MyColorSwatchPicker layout="stack"> <MyColorSwatchPickerItem color="#A00" /> <MyColorSwatchPickerItem color="#f80" /> <MyColorSwatchPickerItem color="#080" /> <MyColorSwatchPickerItem color="#08f" /> <MyColorSwatchPickerItem color="#088" /> <MyColorSwatchPickerItem color="#008" /> </MyColorSwatchPicker> <MyColorSwatchPicker layout="stack"> <MyColorSwatchPickerItem color="#A00" /> <MyColorSwatchPickerItem color="#f80" /> <MyColorSwatchPickerItem color="#080" /> <MyColorSwatchPickerItem color="#08f" /> <MyColorSwatchPickerItem color="#088" /> <MyColorSwatchPickerItem color="#008" /> </MyColorSwatchPicker> <MyColorSwatchPicker layout="stack"> <MyColorSwatchPickerItem color="#A00" /> <MyColorSwatchPickerItem color="#f80" /> <MyColorSwatchPickerItem color="#080" /> <MyColorSwatchPickerItem color="#08f" /> <MyColorSwatchPickerItem color="#088" /> <MyColorSwatchPickerItem color="#008" /> </MyColorSwatchPicker> Show CSS .react-aria-ColorSwatchPicker { &[data-layout=stack] { flex-direction: column; } } .react-aria-ColorSwatchPicker { &[data-layout=stack] { flex-direction: column; } } .react-aria-ColorSwatchPicker { &[data-layout=stack] { flex-direction: column; } } ## Props# * * * ### ColorSwatchPicker# | Name | Type | Default | Description | | --- | --- | --- | --- | | `children` | `ReactNode` | — | The children of the ColorSwatchPicker. | | `layout` | `'grid' | 'stack'` | `'grid'` | Whether the items are arranged in a stack or grid. | | `value` | `string | Color ` | — | The current value (controlled). | | `defaultValue` | `string | Color ` | — | The default value (uncontrolled). | | `className` | `string | ( (values: ColorSwatchPickerRenderProps & & { defaultClassName: string | | undefined } )) => string` | — | The CSS className for the element. A function may be provided to compute the class based on component state. | | `style` | `CSSProperties | ( (values: ColorSwatchPickerRenderProps & & { defaultStyle: CSSProperties } )) => CSSProperties | undefined` | — | The inline style for the element. A function may be provided to compute the style based on component state. | Events | Name | Type | Description | | --- | --- | --- | | `onChange` | `( (value: Color )) => void` | Handler that is called when the value changes. | Accessibility | Name | Type | Description | | --- | --- | --- | | `aria-label` | `string` | Defines a string value that labels the current element. | | `aria-labelledby` | `string` | Identifies the element (or elements) that labels the current element. | | `aria-describedby` | `string` | Identifies the element (or elements) that describes the object. | | `aria-details` | `string` | Identifies the element (or elements) that provide a detailed, extended description for the object. | ### ColorSwatchPickerItem# A `<ColorSwatchPickerItem>` represents an individual item within a `<ColorSwatchPicker>`. It should contain a `<ColorSwatch>`. | Name | Type | Description | | --- | --- | --- | | `color` | `string | Color ` | The color of the swatch. | | `isDisabled` | `boolean` | Whether the color swatch is disabled. | | `children` | `ReactNode | ( (values: ColorSwatchPickerItemRenderProps & & { defaultChildren: ReactNode | | undefined } )) => ReactNode` | The children of the component. A function may be provided to alter the children based on component state. | | `className` | `string | ( (values: ColorSwatchPickerItemRenderProps & & { defaultClassName: string | | undefined } )) => string` | The CSS className for the element. A function may be provided to compute the class based on component state. | | `style` | `CSSProperties | ( (values: ColorSwatchPickerItemRenderProps & & { defaultStyle: CSSProperties } )) => CSSProperties | undefined` | The inline style for the element. A function may be provided to compute the style based on component state. | Events | Name | Type | Description | | --- | --- | --- | | `onHoverStart` | `( (e: HoverEvent )) => void` | Handler that is called when a hover interaction starts. | | `onHoverEnd` | `( (e: HoverEvent )) => void` | Handler that is called when a hover interaction ends. | | `onHoverChange` | `( (isHovering: boolean )) => void` | Handler that is called when the hover state changes. | ## Styling# * * * React Aria components can be styled in many ways, including using CSS classes, inline styles, utility classes (e.g. Tailwind), CSS-in-JS (e.g. Styled Components), etc. By default, all components include a builtin `className` attribute which can be targeted using CSS selectors. These follow the `react-aria-ComponentName` naming convention. .react-aria-ColorSwatchPicker { /* ... */ } .react-aria-ColorSwatchPicker { /* ... */ } .react-aria-ColorSwatchPicker { /* ... */ } A custom `className` can also be specified on any component. This overrides the default `className` provided by React Aria with your own. <ColorSwatchPicker className="my-color-swatch-picker"> {/* ... */} </ColorSwatchPicker> <ColorSwatchPicker className="my-color-swatch-picker"> {/* ... */} </ColorSwatchPicker> <ColorSwatchPicker className="my-color-swatch-picker"> {/* ... */} </ColorSwatchPicker> In addition, some components support multiple UI states (e.g. focused, placeholder, readonly, etc.). React Aria components expose states using data attributes, which you can target in CSS selectors. For example: .react-aria-ColorSwatchPickerItem[data-selected] { /* ... */ } .react-aria-ColorSwatchPickerItem[data-selected] { /* ... */ } .react-aria-ColorSwatchPickerItem[data-selected] { /* ... */ } The `className` and `style` props also accept functions which receive states for styling. This lets you dynamically determine the classes or styles to apply, which is useful when using utility CSS libraries like Tailwind. <ColorSwatchPickerItem className={({ isSelected }) => isSelected ? 'border-black' : 'border-transparent'} /> <ColorSwatchPickerItem className={({ isSelected }) => isSelected ? 'border-black' : 'border-transparent'} /> <ColorSwatchPickerItem className={( { isSelected } ) => isSelected ? 'border-black' : 'border-transparent'} /> The states, selectors, and render props for each component used in a `ColorSwatchPicker` are documented below. ### ColorSwatchPicker# The `ColorSwatchPicker` component can be targeted with the `.react-aria-ColorSwatchPicker` CSS selector, or by overriding with a custom `className`. It supports the following states: | Name | CSS Selector | Description | | --- | --- | --- | | `isEmpty` | `[data-empty]` | Whether the listbox has no items and should display its empty state. | | `isFocused` | `[data-focused]` | Whether the listbox is currently focused. | | `isFocusVisible` | `[data-focus-visible]` | Whether the listbox is currently keyboard focused. | | `layout` | `[data-layout="stack | grid"]` | Whether the items are arranged in a stack or grid. | | `state` | `—` | State of the listbox. | ### ColorSwatchPickerItem# The `ColorSwatchPickerItem` component can be targeted with the `.react-aria-ColorSwatchPickerItem` CSS selector, or by overriding with a custom `className`. It supports the following states: | Name | CSS Selector | Description | | --- | --- | --- | | `color` | `—` | The color of the swatch. | | `isHovered` | `[data-hovered]` | Whether the item is currently hovered with a mouse. | | `isPressed` | `[data-pressed]` | Whether the item is currently in a pressed state. | | `isSelected` | `[data-selected]` | Whether the item is currently selected. | | `isFocused` | `[data-focused]` | Whether the item is currently focused. | | `isFocusVisible` | `[data-focus-visible]` | Whether the item is currently keyboard focused. | | `isDisabled` | `[data-disabled]` | Whether the item is non-interactive, i.e. both selection and actions are disabled and the item may not be focused. Dependent on `disabledKeys` and `disabledBehavior`. | ### ColorSwatch# The `ColorSwatch` component can be targeted with the `.react-aria-ColorSwatch` CSS selector, or by overriding with a custom `className`. It supports the following states: | Name | Description | | --- | --- | | `color` | The color of the swatch. | ## Advanced customization# * * * ### Composition# If you need to customize one of the components within a `ColorSwatchPicker`, such as `ColorSwatchPickerItem` or `ColorSwatch`, in many cases you can create a wrapper component. This lets you customize the props passed to the component. function MyColorSwatchPickerItem(props) { return <ColorSwatchPickerItem {...props} className="my-swatch" /> } function MyColorSwatchPickerItem(props) { return ( <ColorSwatchPickerItem {...props} className="my-swatch" /> ); } function MyColorSwatchPickerItem( props ) { return ( <ColorSwatchPickerItem {...props} className="my-swatch" /> ); } ### Contexts# All React Aria Components export a corresponding context that can be used to send props to them from a parent element. This enables you to build your own compositional APIs similar to those found in React Aria Components itself. You can send any prop or ref via context that you could pass to the corresponding component. The local props and ref on the component are merged with the ones passed via context, with the local props taking precedence (following the rules documented in mergeProps). | Component | Context | Props | Ref | | --- | --- | --- | --- | | `ColorSwatchPicker` | `ColorSwatchPickerContext` | ` ColorSwatchPickerProps ` | `HTMLDivElement` | This example shows how to synchronize a ColorSwatchPicker and a ColorField via context. import {ColorSwatchPickerContext} from 'react-aria-components'; import {MyColorField} from './ColorField'; function ColorSelector({children}) { let [value, setValue] = React.useState(parseColor('#A00')); return ( <div style={{display: 'flex', flexDirection: 'column', gap: 8}}> <MyColorField label="Color" value={value} onChange={setValue} /> <ColorSwatchPickerContext.Provider value={{value, onChange: setValue}}> {children} </ColorSwatchPickerContext.Provider> </div> ); } <ColorSelector> <MyColorSwatchPicker> <MyColorSwatchPickerItem color="#A00" /> <MyColorSwatchPickerItem color="#f80" /> <MyColorSwatchPickerItem color="#080" /> </MyColorSwatchPicker> </ColorSelector> import {ColorSwatchPickerContext} from 'react-aria-components'; import {MyColorField} from './ColorField'; function ColorSelector({ children }) { let [value, setValue] = React.useState( parseColor('#A00') ); return ( <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }} > <MyColorField label="Color" value={value} onChange={setValue} /> <ColorSwatchPickerContext.Provider value={{ value, onChange: setValue }} > {children} </ColorSwatchPickerContext.Provider> </div> ); } <ColorSelector> <MyColorSwatchPicker> <MyColorSwatchPickerItem color="#A00" /> <MyColorSwatchPickerItem color="#f80" /> <MyColorSwatchPickerItem color="#080" /> </MyColorSwatchPicker> </ColorSelector> import {ColorSwatchPickerContext} from 'react-aria-components'; import {MyColorField} from './ColorField'; function ColorSelector( { children } ) { let [value, setValue] = React.useState( parseColor('#A00') ); return ( <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }} > <MyColorField label="Color" value={value} onChange={setValue} /> <ColorSwatchPickerContext.Provider value={{ value, onChange: setValue }} > {children} </ColorSwatchPickerContext.Provider> </div> ); } <ColorSelector> <MyColorSwatchPicker> <MyColorSwatchPickerItem color="#A00" /> <MyColorSwatchPickerItem color="#f80" /> <MyColorSwatchPickerItem color="#080" /> </MyColorSwatchPicker> </ColorSelector> Color --- ## Page: https://react-spectrum.adobe.com/react-aria/ColorWheel.html # ColorWheel A color wheel allows users to adjust the hue of an HSL or HSB color value on a circular track. <table><tbody><tr><th>install</th><td><code>yarn add react-aria-components</code></td></tr><tr><th>version</th><td>1.10.1</td></tr><tr><th>usage</th><td><code><span>import</span> {ColorWheel} <span>from</span> <span>'react-aria-components'</span></code></td></tr></tbody></table> View ARIA pattern W3C View repository GitHub View package NPM ## Example# * * * import {ColorWheel, ColorWheelTrack, ColorThumb} from 'react-aria-components'; <ColorWheel outerRadius={100} innerRadius={74}> <ColorWheelTrack /> <ColorThumb /> </ColorWheel> import { ColorThumb, ColorWheel, ColorWheelTrack } from 'react-aria-components'; <ColorWheel outerRadius={100} innerRadius={74}> <ColorWheelTrack /> <ColorThumb /> </ColorWheel> import { ColorThumb, ColorWheel, ColorWheelTrack } from 'react-aria-components'; <ColorWheel outerRadius={100} innerRadius={74} > <ColorWheelTrack /> <ColorThumb /> </ColorWheel> Show CSS .react-aria-ColorThumb { border: 2px solid white; box-shadow: 0 0 0 1px black, inset 0 0 0 1px black; width: 20px; height: 20px; border-radius: 50%; box-sizing: border-box; &[data-focus-visible] { width: 24px; height: 24px; } } .react-aria-ColorThumb { border: 2px solid white; box-shadow: 0 0 0 1px black, inset 0 0 0 1px black; width: 20px; height: 20px; border-radius: 50%; box-sizing: border-box; &[data-focus-visible] { width: 24px; height: 24px; } } .react-aria-ColorThumb { border: 2px solid white; box-shadow: 0 0 0 1px black, inset 0 0 0 1px black; width: 20px; height: 20px; border-radius: 50%; box-sizing: border-box; &[data-focus-visible] { width: 24px; height: 24px; } } ## Features# * * * The <input type="color"> HTML element can be used to build a color picker, however it is very inconsistent across browsers and operating systems and consists of a complete color picker rather than only a hue color wheel. `useColorWheel` helps achieve accessible and touch-friendly color wheels that can be styled as needed. * **Customizable** – Support for adjusting the hue of an HSL or HSB color value, with customizable radius and track thickness. * **High quality interactions** – Mouse, touch, and keyboard input is supported via the useMove hook. Pressing the track moves the thumb to that position. Text selection and touch scrolling are prevented while dragging. * **Accessible** – Announces localized hue descriptions for screen reader users (e.g. "cyan blue"). Uses a visually hidden `<input>` element for mobile screen reader support and HTML form integration. ## Anatomy# * * * A color wheel consists of a circular track and a thumb that the user can drag to change the color hue. A visually hidden `<input>` element is used to represent the value to assistive technologies. import {ColorWheel, ColorWheelTrack, ColorThumb} from 'react-aria-components'; <ColorWheel> <ColorWheelTrack /> <ColorThumb /> </ColorWheel> import { ColorThumb, ColorWheel, ColorWheelTrack } from 'react-aria-components'; <ColorWheel> <ColorWheelTrack /> <ColorThumb /> </ColorWheel> import { ColorThumb, ColorWheel, ColorWheelTrack } from 'react-aria-components'; <ColorWheel> <ColorWheelTrack /> <ColorThumb /> </ColorWheel> ## Starter kits# * * * To help kick-start your project, we offer starter kits that include example implementations of all React Aria components with various styling solutions. All components are fully styled, including support for dark mode, high contrast mode, and all UI states. Each starter comes with a pre-configured Storybook that you can experiment with, or use as a starting point for your own component library. Vanilla CSS Download ZIP Preview Tailwind CSS Download ZIP Preview ## Reusable wrappers# * * * If you will use a ColorWheel in multiple places in your app, you can wrap all of the pieces into a reusable component. This way, the DOM structure, styling code, and other logic are defined in a single place and reused everywhere to ensure consistency. import type {ColorWheelProps} from 'react-aria-components'; interface MyColorWheelProps extends Omit<ColorWheelProps, 'outerRadius' | 'innerRadius'> {} export function MyColorWheel(props: MyColorWheelProps) { return ( <ColorWheel {...props} outerRadius={100} innerRadius={74}> <ColorWheelTrack /> <ColorThumb /> </ColorWheel> ); } <MyColorWheel defaultValue="hsl(30, 100%, 50%)" /> import type {ColorWheelProps} from 'react-aria-components'; interface MyColorWheelProps extends Omit<ColorWheelProps, 'outerRadius' | 'innerRadius'> {} export function MyColorWheel(props: MyColorWheelProps) { return ( <ColorWheel {...props} outerRadius={100} innerRadius={74} > <ColorWheelTrack /> <ColorThumb /> </ColorWheel> ); } <MyColorWheel defaultValue="hsl(30, 100%, 50%)" /> import type {ColorWheelProps} from 'react-aria-components'; interface MyColorWheelProps extends Omit< ColorWheelProps, | 'outerRadius' | 'innerRadius' > {} export function MyColorWheel( props: MyColorWheelProps ) { return ( <ColorWheel {...props} outerRadius={100} innerRadius={74} > <ColorWheelTrack /> <ColorThumb /> </ColorWheel> ); } <MyColorWheel defaultValue="hsl(30, 100%, 50%)" /> ## Value# * * * A ColorWheel's `value` specifies the position of the ColorWheel's thumb on the track, and accepts a string or `Color` object. ### Uncontrolled# By default, `ColorWheel` is uncontrolled with a default value of red (hue = 0˚). You can change the default value using the `defaultValue` prop. <MyColorWheel defaultValue="hsl(80, 100%, 50%)" /> <MyColorWheel defaultValue="hsl(80, 100%, 50%)" /> <MyColorWheel defaultValue="hsl(80, 100%, 50%)" /> ### Controlled# A `ColorWheel` can be made controlled using the `value` prop. The `parseColor` function is used to parse the initial color from an HSL string, stored in state. The `onChange` prop is used to update the value in state when the user drags the thumb. import {parseColor} from 'react-aria-components'; function Example() { let [color, setColor] = React.useState(parseColor('hsl(0, 100%, 50%)')); return ( <> <MyColorWheel value={color} onChange={setColor} /> <p>Current color value: {color.toString('hsl')}</p> </> ); } import {parseColor} from 'react-aria-components'; function Example() { let [color, setColor] = React.useState( parseColor('hsl(0, 100%, 50%)') ); return ( <> <MyColorWheel value={color} onChange={setColor} /> <p>Current color value: {color.toString('hsl')}</p> </> ); } import {parseColor} from 'react-aria-components'; function Example() { let [color, setColor] = React.useState( parseColor( 'hsl(0, 100%, 50%)' ) ); return ( <> <MyColorWheel value={color} onChange={setColor} /> <p> Current color value:{' '} {color.toString( 'hsl' )} </p> </> ); } Current color value: hsl(0, 100%, 50%) ### HTML forms# ColorWheel supports the `name` prop for integration with HTML forms. The value will be submitted as a number between 0 and 360 degrees. <MyColorWheel name="hue" /> <MyColorWheel name="hue" /> <MyColorWheel name="hue" /> ## Events# * * * ColorWheel supports two events: `onChange` and `onChangeEnd`. `onChange` is triggered whenever the ColorWheel's handle is dragged, and `onChangeEnd` is triggered when the user stops dragging the handle. Both events receive a `Color` object as a parameter. The example below uses `onChange` and `onChangeEnd` to update two separate elements with the ColorWheel's value. function Example() { let [currentValue, setCurrentValue] = React.useState( parseColor('hsl(50, 100%, 50%)') ); let [finalValue, setFinalValue] = React.useState(currentValue); return ( <div> <MyColorWheel value={currentValue} onChange={setCurrentValue} onChangeEnd={setFinalValue} /> <p>Current value: {currentValue.toString('hsl')}</p> <p>Final value: {finalValue.toString('hsl')}</p> </div> ); } function Example() { let [currentValue, setCurrentValue] = React.useState( parseColor('hsl(50, 100%, 50%)') ); let [finalValue, setFinalValue] = React.useState( currentValue ); return ( <div> <MyColorWheel value={currentValue} onChange={setCurrentValue} onChangeEnd={setFinalValue} /> <p>Current value: {currentValue.toString('hsl')}</p> <p>Final value: {finalValue.toString('hsl')}</p> </div> ); } function Example() { let [ currentValue, setCurrentValue ] = React.useState( parseColor( 'hsl(50, 100%, 50%)' ) ); let [ finalValue, setFinalValue ] = React.useState( currentValue ); return ( <div> <MyColorWheel value={currentValue} onChange={setCurrentValue} onChangeEnd={setFinalValue} /> <p> Current value: {' '} {currentValue .toString( 'hsl' )} </p> <p> Final value:{' '} {finalValue .toString( 'hsl' )} </p> </div> ); } Current value: hsl(50, 100%, 50%) Final value: hsl(50, 100%, 50%) ## Disabled# * * * A `ColorWheel` can be disabled using the `isDisabled` prop. This prevents the thumb from being focused or dragged. It's up to you to style your color wheel to appear disabled accordingly. <MyColorWheel defaultValue="hsl(80, 100%, 50%)" isDisabled /> <MyColorWheel defaultValue="hsl(80, 100%, 50%)" isDisabled /> <MyColorWheel defaultValue="hsl(80, 100%, 50%)" isDisabled /> Show CSS .react-aria-ColorWheel { &[data-disabled] { .react-aria-ColorWheelTrack { background: gray !important; } .react-aria-ColorThumb { background: gray !important; opacity: 0.5; } } } .react-aria-ColorWheel { &[data-disabled] { .react-aria-ColorWheelTrack { background: gray !important; } .react-aria-ColorThumb { background: gray !important; opacity: 0.5; } } } .react-aria-ColorWheel { &[data-disabled] { .react-aria-ColorWheelTrack { background: gray !important; } .react-aria-ColorThumb { background: gray !important; opacity: 0.5; } } } ## Labeling# * * * By default, a localized string for the "hue" channel name is used as the `aria-label` for the ColorWheel. If you wish to override this with a more specific label, an `aria-label` or `aria-labelledby` prop may be passed to further identify the element to assistive technologies. For example, for a ColorArea that adjusts a background color you might pass the `aria-label` prop, "Background color". If you provide your own `aria-label` or `aria-labelledby`, be sure to localize the string appropriately. <div style={{ display: 'flex', gap: 8, alignItems: 'end', flexWrap: 'wrap' }}> <MyColorWheel aria-label="Background color" defaultValue="hsl(0, 100%, 50%)" /> <div> <label id="hsl-aria-labelledby-id">Background color</label> <MyColorWheel aria-labelledby="hsl-aria-labelledby-id" defaultValue="hsl(0, 100%, 50%)" /> </div> </div> <div style={{ display: 'flex', gap: 8, alignItems: 'end', flexWrap: 'wrap' }} > <MyColorWheel aria-label="Background color" defaultValue="hsl(0, 100%, 50%)" /> <div> <label id="hsl-aria-labelledby-id"> Background color </label> <MyColorWheel aria-labelledby="hsl-aria-labelledby-id" defaultValue="hsl(0, 100%, 50%)" /> </div> </div> <div style={{ display: 'flex', gap: 8, alignItems: 'end', flexWrap: 'wrap' }} > <MyColorWheel aria-label="Background color" defaultValue="hsl(0, 100%, 50%)" /> <div> <label id="hsl-aria-labelledby-id"> Background color </label> <MyColorWheel aria-labelledby="hsl-aria-labelledby-id" defaultValue="hsl(0, 100%, 50%)" /> </div> </div> Background color ### Accessibility# The `aria-valuetext` of the `<input>` element is formatted according to the user's locale automatically. It also includes a localized description of the selected color hue (e.g. "cyan blue"). ## Props# * * * ### ColorWheel# | Name | Type | Default | Description | | --- | --- | --- | --- | | `outerRadius` | `number` | — | The outer radius of the color wheel. | | `innerRadius` | `number` | — | The inner radius of the color wheel. | | `isDisabled` | `boolean` | — | Whether the ColorWheel is disabled. | | `defaultValue` | `string | Color ` | `'hsl(0, 100%, 50%)'` | The default value (uncontrolled). | | `value` | `T` | — | The current value (controlled). | | `name` | `string` | — | The name of the input element, used when submitting an HTML form. See MDN. | | `children` | `ReactNode | ( (values: ColorWheelRenderProps & & { defaultChildren: ReactNode | | undefined } )) => ReactNode` | — | The children of the component. A function may be provided to alter the children based on component state. | | `className` | `string | ( (values: ColorWheelRenderProps & & { defaultClassName: string | | undefined } )) => string` | — | The CSS className for the element. A function may be provided to compute the class based on component state. | | `style` | `CSSProperties | ( (values: ColorWheelRenderProps & & { defaultStyle: CSSProperties } )) => CSSProperties | undefined` | — | The inline style for the element. A function may be provided to compute the style based on component state. | Events | Name | Type | Description | | --- | --- | --- | | `onChange` | `( (value: Color )) => void` | Handler that is called when the value changes, as the user drags. | | `onChangeEnd` | `( (value: Color )) => void` | Handler that is called when the user stops dragging. | Layout | Name | Type | Description | | --- | --- | --- | | `slot` | `string | null` | A slot name for the component. Slots allow the component to receive props from a parent component. An explicit `null` value indicates that the local props completely override all props received from a parent. | Accessibility | Name | Type | Description | | --- | --- | --- | | `id` | `string` | The element's unique identifier. See MDN. | | `aria-label` | `string` | Defines a string value that labels the current element. | | `aria-labelledby` | `string` | Identifies the element (or elements) that labels the current element. | | `aria-describedby` | `string` | Identifies the element (or elements) that describes the object. | | `aria-details` | `string` | Identifies the element (or elements) that provide a detailed, extended description for the object. | ### ColorWheelTrack# The `<ColorWheelTrack>` component renders a circular gradient representing the colors that can be selected for the color channel. Show props | Name | Type | Description | | --- | --- | --- | | `className` | `string | ( (values: ColorWheelTrackRenderProps & & { defaultClassName: string | | undefined } )) => string` | The CSS className for the element. A function may be provided to compute the class based on component state. | | `style` | `CSSProperties | ( (values: ColorWheelTrackRenderProps & & { defaultStyle: CSSProperties } )) => CSSProperties | undefined` | The inline style for the element. A function may be provided to compute the style based on component state. | ### ColorThumb# The `<ColorThumb>` component renders a draggable thumb with a preview of the selected color. Show props | Name | Type | Description | | --- | --- | --- | | `children` | `ReactNode | ( (values: ColorThumbRenderProps & & { defaultChildren: ReactNode | | undefined } )) => ReactNode` | The children of the component. A function may be provided to alter the children based on component state. | | `className` | `string | ( (values: ColorThumbRenderProps & & { defaultClassName: string | | undefined } )) => string` | The CSS className for the element. A function may be provided to compute the class based on component state. | | `style` | `CSSProperties | ( (values: ColorThumbRenderProps & & { defaultStyle: CSSProperties } )) => CSSProperties | undefined` | The inline style for the element. A function may be provided to compute the style based on component state. | Events | Name | Type | Description | | --- | --- | --- | | `onHoverStart` | `( (e: HoverEvent )) => void` | Handler that is called when a hover interaction starts. | | `onHoverEnd` | `( (e: HoverEvent )) => void` | Handler that is called when a hover interaction ends. | | `onHoverChange` | `( (isHovering: boolean )) => void` | Handler that is called when the hover state changes. | ## Styling# * * * React Aria components can be styled in many ways, including using CSS classes, inline styles, utility classes (e.g. Tailwind), CSS-in-JS (e.g. Styled Components), etc. By default, all components include a builtin `className` attribute which can be targeted using CSS selectors. These follow the `react-aria-ComponentName` naming convention. .react-aria-ColorWheel { /* ... */ } .react-aria-ColorWheel { /* ... */ } .react-aria-ColorWheel { /* ... */ } A custom `className` can also be specified on any component. This overrides the default `className` provided by React Aria with your own. <ColorWheel className="my-color-wheel"> {/* ... */} </ColorWheel> <ColorWheel className="my-color-wheel"> {/* ... */} </ColorWheel> <ColorWheel className="my-color-wheel"> {/* ... */} </ColorWheel> In addition, some components support multiple UI states (e.g. focused, placeholder, readonly, etc.). React Aria components expose states using data attributes, which you can target in CSS selectors. For example: .react-aria-ColorThumb[data-dragging] { /* ... */ } .react-aria-ColorThumb[data-focused] { /* ... */ } .react-aria-ColorThumb[data-dragging] { /* ... */ } .react-aria-ColorThumb[data-focused] { /* ... */ } .react-aria-ColorThumb[data-dragging] { /* ... */ } .react-aria-ColorThumb[data-focused] { /* ... */ } The `className` and `style` props also accept functions which receive states for styling. This lets you dynamically determine the classes or styles to apply, which is useful when using utility CSS libraries like Tailwind. <ColorThumb className={({ isDragging }) => isDragging ? 'scale-150' : 'scale-100'} /> <ColorThumb className={({ isDragging }) => isDragging ? 'scale-150' : 'scale-100'} /> <ColorThumb className={( { isDragging } ) => isDragging ? 'scale-150' : 'scale-100'} /> The states, selectors, and render props for each component used in a `ColorWheel` are documented below. ### ColorWheel# The `ColorWheel` component can be targeted with the `.react-aria-ColorWheel` CSS selector, or by overriding with a custom `className`. It supports the following states: | Name | CSS Selector | Description | | --- | --- | --- | | `isDisabled` | `[data-disabled]` | Whether the color wheel is disabled. | | `state` | `—` | State of the color color wheel. | ### ColorWheelTrack# The `ColorWheelTrack` component can be targeted with the `.react-aria-ColorWheelTrack` CSS selector, or by overriding with a custom `className`. It supports the following states: | Name | CSS Selector | Description | | --- | --- | --- | | `isDisabled` | `[data-disabled]` | Whether the color wheel is disabled. | | `state` | `—` | State of the color color wheel. | ### ColorThumb# The `ColorThumb` component can be targeted with the `.react-aria-ColorThumb` CSS selector, or by overriding with a custom `className`. It supports the following states: | Name | CSS Selector | Description | | --- | --- | --- | | `color` | `—` | The selected color, excluding the alpha channel. | | `isDragging` | `[data-dragging]` | Whether this thumb is currently being dragged. | | `isHovered` | `[data-hovered]` | Whether the thumb is currently hovered with a mouse. | | `isFocused` | `[data-focused]` | Whether the thumb is currently focused. | | `isFocusVisible` | `[data-focus-visible]` | Whether the thumb is keyboard focused. | | `isDisabled` | `[data-disabled]` | Whether the thumb is disabled. | ## Advanced customization# * * * ### Contexts# All React Aria Components export a corresponding context that can be used to send props to them from a parent element. This enables you to build your own compositional APIs similar to those found in React Aria Components itself. You can send any prop or ref via context that you could pass to the corresponding component. The local props and ref on the component are merged with the ones passed via context, with the local props taking precedence (following the rules documented in mergeProps). | Component | Context | Props | Ref | | --- | --- | --- | --- | | `ColorWheel` | `ColorWheelContext` | ` ColorWheelProps ` | `HTMLDivElement` | This example shows a `ColorWheelDescription` component that accepts a color wheel in its children and renders a description element below it. It uses the useId hook to generate a unique id for the description, and associates it with the color wheel via the `aria-describedby` attribute passed to the `ColorWheelContext` provider. import {ColorWheelContext} from 'react-aria-components'; import {useId} from 'react-aria'; interface ColorWheelDescriptionProps { children?: React.ReactNode; description?: string; } function ColorWheelDescription( { children, description }: ColorWheelDescriptionProps ) { let descriptionId = useId(); return ( <div> <ColorWheelContext.Provider value={{ 'aria-describedby': descriptionId }}> {children} </ColorWheelContext.Provider> <small id={descriptionId}>{description}</small> </div> ); } <ColorWheelDescription description="Choose a background color for your profile."> <MyColorWheel /> </ColorWheelDescription> import {ColorWheelContext} from 'react-aria-components'; import {useId} from 'react-aria'; interface ColorWheelDescriptionProps { children?: React.ReactNode; description?: string; } function ColorWheelDescription( { children, description }: ColorWheelDescriptionProps ) { let descriptionId = useId(); return ( <div> <ColorWheelContext.Provider value={{ 'aria-describedby': descriptionId }} > {children} </ColorWheelContext.Provider> <small id={descriptionId}>{description}</small> </div> ); } <ColorWheelDescription description="Choose a background color for your profile."> <MyColorWheel /> </ColorWheelDescription> import {ColorWheelContext} from 'react-aria-components'; import {useId} from 'react-aria'; interface ColorWheelDescriptionProps { children?: React.ReactNode; description?: string; } function ColorWheelDescription( { children, description }: ColorWheelDescriptionProps ) { let descriptionId = useId(); return ( <div> <ColorWheelContext.Provider value={{ 'aria-describedby': descriptionId }} > {children} </ColorWheelContext.Provider> <small id={descriptionId} > {description} </small> </div> ); } <ColorWheelDescription description="Choose a background color for your profile."> <MyColorWheel /> </ColorWheelDescription> Choose a background color for your profile. ### State# ColorWheel provides a `ColorWheelState` object to its children via `ColorWheelStateContext`. This can be used to access and manipulate the color wheel's state. This example shows a `HueField` component that can be placed within a `ColorWheel` to allow the user to enter a number and update the hue. import {ColorWheelStateContext, Input, NumberField, useLocale} from 'react-aria-components'; function HueField() { let state = React.useContext(ColorWheelStateContext)!; let { locale } = useLocale(); return ( <NumberField aria-label={state.value.getChannelName('hue', locale)} value={state.value.getChannelValue('hue')} onChange={(v) => state.setValue(state.value.withChannelValue('hue', v))} formatOptions={state.value.getChannelFormatOptions('hue')} > <Input /> </NumberField> ); } <ColorWheel outerRadius={100} innerRadius={74}> <ColorWheelTrack /> <ColorThumb /> <HueField /></ColorWheel> import { ColorWheelStateContext, Input, NumberField, useLocale } from 'react-aria-components'; function HueField() { let state = React.useContext(ColorWheelStateContext)!; let { locale } = useLocale(); return ( <NumberField aria-label={state.value.getChannelName('hue', locale)} value={state.value.getChannelValue('hue')} onChange={(v) => state.setValue( state.value.withChannelValue('hue', v) )} formatOptions={state.value.getChannelFormatOptions( 'hue' )} > <Input /> </NumberField> ); } <ColorWheel outerRadius={100} innerRadius={74}> <ColorWheelTrack /> <ColorThumb /> <HueField /></ColorWheel> import { ColorWheelStateContext, Input, NumberField, useLocale } from 'react-aria-components'; function HueField() { let state = React .useContext( ColorWheelStateContext )!; let { locale } = useLocale(); return ( <NumberField aria-label={state .value .getChannelName( 'hue', locale )} value={state.value .getChannelValue( 'hue' )} onChange={(v) => state.setValue( state.value .withChannelValue( 'hue', v ) )} formatOptions={state .value .getChannelFormatOptions( 'hue' )} > <Input /> </NumberField> ); } <ColorWheel outerRadius={100} innerRadius={74} > <ColorWheelTrack /> <ColorThumb /> <HueField /></ColorWheel> Show CSS .react-aria-Input { width: 4ch; } .react-aria-Input { width: 4ch; } .react-aria-Input { width: 4ch; } ### Hooks# If you need to customize things even further, such as accessing internal state or customizing DOM structure, you can drop down to the lower level Hook-based API. See useColorWheel for more details. --- ## Page: https://react-spectrum.adobe.com/react-aria/Calendar.html # Calendar A calendar displays one or more date grids and allows users to select a single date. <table><tbody><tr><th>install</th><td><code>yarn add react-aria-components</code></td></tr><tr><th>version</th><td>1.10.1</td></tr><tr><th>usage</th><td><code><span>import</span> {Calendar} <span>from</span> <span>'react-aria-components'</span></code></td></tr></tbody></table> View repository GitHub View package NPM ## Example# * * * import {Button, Calendar, CalendarCell, CalendarGrid, Heading} from 'react-aria-components'; <Calendar aria-label="Appointment date"> <header> <Button slot="previous">◀</Button> <Heading /> <Button slot="next">▶</Button> </header> <CalendarGrid> {(date) => <CalendarCell date={date} />} </CalendarGrid> </Calendar> import { Button, Calendar, CalendarCell, CalendarGrid, Heading } from 'react-aria-components'; <Calendar aria-label="Appointment date"> <header> <Button slot="previous">◀</Button> <Heading /> <Button slot="next">▶</Button> </header> <CalendarGrid> {(date) => <CalendarCell date={date} />} </CalendarGrid> </Calendar> import { Button, Calendar, CalendarCell, CalendarGrid, Heading } from 'react-aria-components'; <Calendar aria-label="Appointment date"> <header> <Button slot="previous"> ◀ </Button> <Heading /> <Button slot="next"> ▶ </Button> </header> <CalendarGrid> {(date) => ( <CalendarCell date={date} /> )} </CalendarGrid> </Calendar> ## Appointment date, June 2025 ◀ ## June 2025 ▶ | S | M | T | W | T | F | S | | --- | --- | --- | --- | --- | --- | --- | | 1 | 2 | 3 | 4 | 5 | 6 | 7 | | 8 | 9 | 10 | 11 | 12 | 13 | 14 | | 15 | 16 | 17 | 18 | 19 | 20 | 21 | | 22 | 23 | 24 | 25 | 26 | 27 | 28 | | 29 | 30 | 1 | 2 | 3 | 4 | 5 | Show CSS @import "@react-aria/example-theme"; .react-aria-Calendar { width: fit-content; max-width: 100%; color: var(--text-color); header { display: flex; align-items: center; margin: 0 4px .5rem 4px; .react-aria-Heading { flex: 1; margin: 0; text-align: center; font-size: 1.375rem; } } .react-aria-Button { width: 2rem; height: 2rem; padding: 0; } .react-aria-CalendarCell { width: 2rem; line-height: 2rem; text-align: center; border-radius: 6px; cursor: default; outline: none; margin: 1px; forced-color-adjust: none; &[data-outside-month] { display: none; } &[data-pressed] { background: var(--gray-100); } &[data-focus-visible] { outline: 2px solid var(--focus-ring-color); outline-offset: 2px; } &[data-selected] { background: var(--highlight-background); color: var(--highlight-foreground); } } } @import "@react-aria/example-theme"; .react-aria-Calendar { width: fit-content; max-width: 100%; color: var(--text-color); header { display: flex; align-items: center; margin: 0 4px .5rem 4px; .react-aria-Heading { flex: 1; margin: 0; text-align: center; font-size: 1.375rem; } } .react-aria-Button { width: 2rem; height: 2rem; padding: 0; } .react-aria-CalendarCell { width: 2rem; line-height: 2rem; text-align: center; border-radius: 6px; cursor: default; outline: none; margin: 1px; forced-color-adjust: none; &[data-outside-month] { display: none; } &[data-pressed] { background: var(--gray-100); } &[data-focus-visible] { outline: 2px solid var(--focus-ring-color); outline-offset: 2px; } &[data-selected] { background: var(--highlight-background); color: var(--highlight-foreground); } } } @import "@react-aria/example-theme"; .react-aria-Calendar { width: fit-content; max-width: 100%; color: var(--text-color); header { display: flex; align-items: center; margin: 0 4px .5rem 4px; .react-aria-Heading { flex: 1; margin: 0; text-align: center; font-size: 1.375rem; } } .react-aria-Button { width: 2rem; height: 2rem; padding: 0; } .react-aria-CalendarCell { width: 2rem; line-height: 2rem; text-align: center; border-radius: 6px; cursor: default; outline: none; margin: 1px; forced-color-adjust: none; &[data-outside-month] { display: none; } &[data-pressed] { background: var(--gray-100); } &[data-focus-visible] { outline: 2px solid var(--focus-ring-color); outline-offset: 2px; } &[data-selected] { background: var(--highlight-background); color: var(--highlight-foreground); } } } ## Features# * * * There is no standalone calendar element in HTML. `<input type="date">` is close, but this is very limited in functionality, lacking in internationalization capabilities, inconsistent between browsers, and difficult to style. `Calendar` helps achieve accessible and international calendar components that can be styled as needed. * **Flexible** – Display one or more months at once, or a custom time range for use cases like a week view. Minimum and maximum values, unavailable dates, and non-contiguous selections are supported as well. * **International** – Support for 13 calendar systems used around the world, including Gregorian, Buddhist, Islamic, Persian, and more. Locale-specific formatting, number systems, and right-to-left support are available as well. * **Accessible** – Calendar cells can be navigated and selected using the keyboard, and localized screen reader messages are included to announce when the selection and visible date range change. * **Customizable** – As with all of React Aria, the DOM structure and styling of all elements can be fully customized. Read our blog post for more details about the internationalization, accessibility, and user experience features implemented by `Calendar`. ## Anatomy# * * * A calendar consists of a grouping element containing one or more date grids (e.g. months), and a previous and next button for navigating between date ranges. Each calendar grid consists of cells containing button elements that can be pressed and navigated to using the arrow keys to select a date. `Calendar` also supports an optional error message element, which can be used to provide more context about any validation errors. This is linked with the calendar via the `aria-describedby` attribute. import {Button, Calendar, CalendarCell, CalendarGrid, CalendarGridBody, CalendarGridHeader, CalendarHeaderCell, Heading, Text} from 'react-aria-components'; <Calendar> <Button slot="previous" /> <Heading /> <Button slot="next" /> <CalendarGrid> <CalendarGridHeader> {(day) => <CalendarHeaderCell />} </CalendarGridHeader> <CalendarGridBody> {(date) => <CalendarCell date={date} />} </CalendarGridBody> </CalendarGrid> <Text slot="errorMessage" /> </Calendar> import { Button, Calendar, CalendarCell, CalendarGrid, CalendarGridBody, CalendarGridHeader, CalendarHeaderCell, Heading, Text } from 'react-aria-components'; <Calendar> <Button slot="previous" /> <Heading /> <Button slot="next" /> <CalendarGrid> <CalendarGridHeader> {(day) => <CalendarHeaderCell />} </CalendarGridHeader> <CalendarGridBody> {(date) => <CalendarCell date={date} />} </CalendarGridBody> </CalendarGrid> <Text slot="errorMessage" /> </Calendar> import { Button, Calendar, CalendarCell, CalendarGrid, CalendarGridBody, CalendarGridHeader, CalendarHeaderCell, Heading, Text } from 'react-aria-components'; <Calendar> <Button slot="previous" /> <Heading /> <Button slot="next" /> <CalendarGrid> <CalendarGridHeader> {(day) => ( <CalendarHeaderCell /> )} </CalendarGridHeader> <CalendarGridBody> {(date) => ( <CalendarCell date={date} /> )} </CalendarGridBody> </CalendarGrid> <Text slot="errorMessage" /> </Calendar> Note that much of this anatomy is shared with range calendars. If you have both, the styling and internal components such as `CalendarCell` can be shared. ### Concepts# `Calendar` makes use of the following concepts: @internationalized/date Represent and manipulate dates and times in a locale-aware manner. ### Composed components# A `Calendar` uses the following components, which may also be used standalone or reused in other components. Button A button allows a user to perform an action. ## Starter kits# * * * To help kick-start your project, we offer starter kits that include example implementations of all React Aria components with various styling solutions. All components are fully styled, including support for dark mode, high contrast mode, and all UI states. Each starter comes with a pre-configured Storybook that you can experiment with, or use as a starting point for your own component library. Vanilla CSS Download ZIP Preview Tailwind CSS Download ZIP Preview ## Reusable wrappers# * * * If you will use a Calendar in multiple places in your app, you can wrap all of the pieces into a reusable component. This way, the DOM structure, styling code, and other logic are defined in a single place and reused everywhere to ensure consistency. This example also shows how to use the `errorMessage` slot to render help text in case of a validation error (see below for details). import type {CalendarProps, DateValue} from 'react-aria-components'; import {Text} from 'react-aria-components'; interface MyCalendarProps<T extends DateValue> extends CalendarProps<T> { errorMessage?: string; } function MyCalendar<T extends DateValue>( { errorMessage, ...props }: MyCalendarProps<T> ) { return ( <Calendar {...props}> <header> <Button slot="previous">◀</Button> <Heading /> <Button slot="next">▶</Button> </header> <CalendarGrid> {(date) => <CalendarCell date={date} />} </CalendarGrid> {errorMessage && <Text slot="errorMessage">{errorMessage}</Text>} </Calendar> ); } <MyCalendar aria-label="Event date" /> import type { CalendarProps, DateValue } from 'react-aria-components'; import {Text} from 'react-aria-components'; interface MyCalendarProps<T extends DateValue> extends CalendarProps<T> { errorMessage?: string; } function MyCalendar<T extends DateValue>( { errorMessage, ...props }: MyCalendarProps<T> ) { return ( <Calendar {...props}> <header> <Button slot="previous">◀</Button> <Heading /> <Button slot="next">▶</Button> </header> <CalendarGrid> {(date) => <CalendarCell date={date} />} </CalendarGrid> {errorMessage && ( <Text slot="errorMessage">{errorMessage}</Text> )} </Calendar> ); } <MyCalendar aria-label="Event date" /> import type { CalendarProps, DateValue } from 'react-aria-components'; import {Text} from 'react-aria-components'; interface MyCalendarProps< T extends DateValue > extends CalendarProps<T> { errorMessage?: string; } function MyCalendar< T extends DateValue >( { errorMessage, ...props }: MyCalendarProps<T> ) { return ( <Calendar {...props}> <header> <Button slot="previous"> ◀ </Button> <Heading /> <Button slot="next"> ▶ </Button> </header> <CalendarGrid> {(date) => ( <CalendarCell date={date} /> )} </CalendarGrid> {errorMessage && ( <Text slot="errorMessage"> {errorMessage} </Text> )} </Calendar> ); } <MyCalendar aria-label="Event date" /> ## Event date, June 2025 ◀ ## June 2025 ▶ | S | M | T | W | T | F | S | | --- | --- | --- | --- | --- | --- | --- | | 1 | 2 | 3 | 4 | 5 | 6 | 7 | | 8 | 9 | 10 | 11 | 12 | 13 | 14 | | 15 | 16 | 17 | 18 | 19 | 20 | 21 | | 22 | 23 | 24 | 25 | 26 | 27 | 28 | | 29 | 30 | 1 | 2 | 3 | 4 | 5 | ## Value# * * * A `Calendar` has no selection by default. An initial, uncontrolled value can be provided to the `Calendar` using the `defaultValue` prop. Alternatively, a controlled value can be provided using the `value` prop. Date values are provided using objects in the @internationalized/date package. This library handles correct international date manipulation across calendars, time zones, and other localization concerns. `Calendar` supports values with both date and time components, but only allows users to modify the date. By default, `Calendar` will emit `CalendarDate` objects in the `onChange` event, but if a` CalendarDateTime `or` ZonedDateTime `object is passed as the `value` or `defaultValue`, values of that type will be emitted, changing only the date and preserving the time components. import {parseDate} from '@internationalized/date'; function Example() { let [value, setValue] = React.useState(parseDate('2020-02-03')); return ( <div style={{display: 'flex', gap: 20, flexWrap: 'wrap'}}> <MyCalendar aria-label="Date (uncontrolled)" defaultValue={parseDate('2020-02-03')} /> <MyCalendar aria-label="Date (controlled)" value={value} onChange={setValue} /> </div> ); } import {parseDate} from '@internationalized/date'; function Example() { let [value, setValue] = React.useState( parseDate('2020-02-03') ); return ( <div style={{ display: 'flex', gap: 20, flexWrap: 'wrap' }} > <MyCalendar aria-label="Date (uncontrolled)" defaultValue={parseDate('2020-02-03')} /> <MyCalendar aria-label="Date (controlled)" value={value} onChange={setValue} /> </div> ); } import {parseDate} from '@internationalized/date'; function Example() { let [value, setValue] = React.useState( parseDate( '2020-02-03' ) ); return ( <div style={{ display: 'flex', gap: 20, flexWrap: 'wrap' }} > <MyCalendar aria-label="Date (uncontrolled)" defaultValue={parseDate( '2020-02-03' )} /> <MyCalendar aria-label="Date (controlled)" value={value} onChange={setValue} /> </div> ); } ## Date (uncontrolled), February 2020 ◀ ## February 2020 ▶ | S | M | T | W | T | F | S | | --- | --- | --- | --- | --- | --- | --- | | 26 | 27 | 28 | 29 | 30 | 31 | 1 | | 2 | 3 | 4 | 5 | 6 | 7 | 8 | | 9 | 10 | 11 | 12 | 13 | 14 | 15 | | 16 | 17 | 18 | 19 | 20 | 21 | 22 | | 23 | 24 | 25 | 26 | 27 | 28 | 29 | ## Date (controlled), February 2020 ◀ ## February 2020 ▶ | S | M | T | W | T | F | S | | --- | --- | --- | --- | --- | --- | --- | | 26 | 27 | 28 | 29 | 30 | 31 | 1 | | 2 | 3 | 4 | 5 | 6 | 7 | 8 | | 9 | 10 | 11 | 12 | 13 | 14 | 15 | | 16 | 17 | 18 | 19 | 20 | 21 | 22 | | 23 | 24 | 25 | 26 | 27 | 28 | 29 | ### International calendars# `Calendar` supports selecting dates in many calendar systems used around the world, including Gregorian, Hebrew, Indian, Islamic, Buddhist, and more. Dates are automatically displayed in the appropriate calendar system for the user's locale. The calendar system can be overridden using the Unicode calendar locale extension, passed to the I18nProvider component. Selected dates passed to `onChange` always use the same calendar system as the `value` or `defaultValue` prop. If no `value` or `defaultValue` is provided, then dates passed to `onChange` are always in the Gregorian calendar since this is the most commonly used. This means that even though the user selects dates in their local calendar system, applications are able to deal with dates from all users consistently. The below example displays a `Calendar` in the Hindi language, using the Indian calendar. Dates emitted from `onChange` are in the Gregorian calendar. import {I18nProvider} from 'react-aria'; function Example() { let [date, setDate] = React.useState<DateValue | null>(null); return ( <I18nProvider locale="hi-IN-u-ca-indian"> <MyCalendar aria-label="Date" value={date} onChange={setDate} /> <p>Selected date: {date?.toString()}</p> </I18nProvider> ); } import {I18nProvider} from 'react-aria'; function Example() { let [date, setDate] = React.useState<DateValue | null>( null ); return ( <I18nProvider locale="hi-IN-u-ca-indian"> <MyCalendar aria-label="Date" value={date} onChange={setDate} /> <p>Selected date: {date?.toString()}</p> </I18nProvider> ); } import {I18nProvider} from 'react-aria'; function Example() { let [date, setDate] = React.useState< DateValue | null >(null); return ( <I18nProvider locale="hi-IN-u-ca-indian"> <MyCalendar aria-label="Date" value={date} onChange={setDate} /> <p> Selected date: {' '} {date ?.toString()} </p> </I18nProvider> ); } ## Date, शक 1947 ज्येष्ठ ◀ ## शक 1947 ज्येष्ठ ▶ | र | सो | मं | बु | गु | शु | श | | --- | --- | --- | --- | --- | --- | --- | | 28 | 29 | 30 | 31 | 1 | 2 | 3 | | 4 | 5 | 6 | 7 | 8 | 9 | 10 | | 11 | 12 | 13 | 14 | 15 | 16 | 17 | | 18 | 19 | 20 | 21 | 22 | 23 | 24 | | 25 | 26 | 27 | 28 | 29 | 30 | 31 | Selected date: ### Custom calendar systems# `Calendar` also supports custom calendar systems that implement custom business rules. An example would be a fiscal year calendar that follows a 4-5-4 format, where month ranges don't follow the usual Gregorian calendar. The `createCalendar` prop accepts a function that returns an instance of the `Calendar` interface. See the @internationalized/date docs for an example implementation. import {GregorianCalendar} from '@internationalized/date'; function Example() { return ( <MyCalendar firstDayOfWeek="sun" createCalendar={() => new Custom454()} /> ); } class Custom454 extends GregorianCalendar { // See @internationalized/date docs linked above... } import {GregorianCalendar} from '@internationalized/date'; function Example() { return ( <MyCalendar firstDayOfWeek="sun" createCalendar={() => new Custom454()} /> ); } class Custom454 extends GregorianCalendar { // See @internationalized/date docs linked above... } import {GregorianCalendar} from '@internationalized/date'; function Example() { return ( <MyCalendar firstDayOfWeek="sun" createCalendar={() => new Custom454()} /> ); } class Custom454 extends GregorianCalendar { // See @internationalized/date docs linked above... } ## June 2025 ◀ ## June 2025 ▶ | S | M | T | W | T | F | S | | --- | --- | --- | --- | --- | --- | --- | | 1 | 2 | 3 | 4 | 5 | 6 | 7 | | 8 | 9 | 10 | 11 | 12 | 13 | 14 | | 15 | 16 | 17 | 18 | 19 | 20 | 21 | | 22 | 23 | 24 | 25 | 26 | 27 | 28 | ## Events# * * * `Calendar` accepts an `onChange` prop which is triggered whenever a date is selected by the user. The example below uses `onChange` to update a separate element with a formatted version of the date in the user's locale. This is done by converting the date to a native JavaScript `Date` object to pass to the formatter. import {useDateFormatter} from 'react-aria'; import {getLocalTimeZone} from '@internationalized/date'; function Example() { let [date, setDate] = React.useState(parseDate('2022-07-04')); let formatter = useDateFormatter({ dateStyle: 'full' }); return ( <> <MyCalendar aria-label="Event date" value={date} onChange={setDate} /> <p>Selected date: {formatter.format(date.toDate(getLocalTimeZone()))}</p> </> ); } import {useDateFormatter} from 'react-aria'; import {getLocalTimeZone} from '@internationalized/date'; function Example() { let [date, setDate] = React.useState( parseDate('2022-07-04') ); let formatter = useDateFormatter({ dateStyle: 'full' }); return ( <> <MyCalendar aria-label="Event date" value={date} onChange={setDate} /> <p> Selected date:{' '} {formatter.format(date.toDate(getLocalTimeZone()))} </p> </> ); } import {useDateFormatter} from 'react-aria'; import {getLocalTimeZone} from '@internationalized/date'; function Example() { let [date, setDate] = React.useState( parseDate( '2022-07-04' ) ); let formatter = useDateFormatter({ dateStyle: 'full' }); return ( <> <MyCalendar aria-label="Event date" value={date} onChange={setDate} /> <p> Selected date: {' '} {formatter .format( date.toDate( getLocalTimeZone() ) )} </p> </> ); } ## Event date, July 2022 ◀ ## July 2022 ▶ | S | M | T | W | T | F | S | | --- | --- | --- | --- | --- | --- | --- | | 26 | 27 | 28 | 29 | 30 | 1 | 2 | | 3 | 4 | 5 | 6 | 7 | 8 | 9 | | 10 | 11 | 12 | 13 | 14 | 15 | 16 | | 17 | 18 | 19 | 20 | 21 | 22 | 23 | | 24 | 25 | 26 | 27 | 28 | 29 | 30 | | 31 | 1 | 2 | 3 | 4 | 5 | 6 | Selected date: Monday, July 4, 2022 ## Multi-month# * * * Multiple `CalendarGrid` elements can be rendered to show multiple months at once. The `visibleDuration` prop should be provided to the `Calendar` component to specify how many months are visible, and each `CalendarGrid` accepts an `offset` prop to specify its starting date relative to the first visible date. <Calendar aria-label="Appointment date" visibleDuration={{months: 3}}> <header> <Button slot="previous">◀</Button> <Heading /> <Button slot="next">▶</Button> </header> <div style={{display: 'flex', gap: 30, overflow: 'auto'}}> <CalendarGrid> {date => <CalendarCell date={date} />} </CalendarGrid> <CalendarGrid offset={{months: 1}}> {date => <CalendarCell date={date} />} </CalendarGrid> <CalendarGrid offset={{months: 2}}> {date => <CalendarCell date={date} />} </CalendarGrid> </div> </Calendar> <Calendar aria-label="Appointment date" visibleDuration={{ months: 3 }} > <header> <Button slot="previous">◀</Button> <Heading /> <Button slot="next">▶</Button> </header> <div style={{ display: 'flex', gap: 30, overflow: 'auto' }} > <CalendarGrid> {(date) => <CalendarCell date={date} />} </CalendarGrid> <CalendarGrid offset={{ months: 1 }}> {(date) => <CalendarCell date={date} />} </CalendarGrid> <CalendarGrid offset={{ months: 2 }}> {(date) => <CalendarCell date={date} />} </CalendarGrid> </div> </Calendar> <Calendar aria-label="Appointment date" visibleDuration={{ months: 3 }} > <header> <Button slot="previous"> ◀ </Button> <Heading /> <Button slot="next"> ▶ </Button> </header> <div style={{ display: 'flex', gap: 30, overflow: 'auto' }} > <CalendarGrid> {(date) => ( <CalendarCell date={date} /> )} </CalendarGrid> <CalendarGrid offset={{ months: 1 }} > {(date) => ( <CalendarCell date={date} /> )} </CalendarGrid> <CalendarGrid offset={{ months: 2 }} > {(date) => ( <CalendarCell date={date} /> )} </CalendarGrid> </div> </Calendar> ## Appointment date, May to July 2025 ◀ ## May – July 2025 ▶ | S | M | T | W | T | F | S | | --- | --- | --- | --- | --- | --- | --- | | 27 | 28 | 29 | 30 | 1 | 2 | 3 | | 4 | 5 | 6 | 7 | 8 | 9 | 10 | | 11 | 12 | 13 | 14 | 15 | 16 | 17 | | 18 | 19 | 20 | 21 | 22 | 23 | 24 | | 25 | 26 | 27 | 28 | 29 | 30 | 31 | | S | M | T | W | T | F | S | | --- | --- | --- | --- | --- | --- | --- | | 1 | 2 | 3 | 4 | 5 | 6 | 7 | | 8 | 9 | 10 | 11 | 12 | 13 | 14 | | 15 | 16 | 17 | 18 | 19 | 20 | 21 | | 22 | 23 | 24 | 25 | 26 | 27 | 28 | | 29 | 30 | 1 | 2 | 3 | 4 | 5 | | S | M | T | W | T | F | S | | --- | --- | --- | --- | --- | --- | --- | | 29 | 30 | 1 | 2 | 3 | 4 | 5 | | 6 | 7 | 8 | 9 | 10 | 11 | 12 | | 13 | 14 | 15 | 16 | 17 | 18 | 19 | | 20 | 21 | 22 | 23 | 24 | 25 | 26 | | 27 | 28 | 29 | 30 | 31 | 1 | 2 | ### Page behavior# The `pageBehavior` prop allows you to control how the calendar navigates between months. By default, the calendar will navigate by `visibleDuration`, but by setting `pageBehavior` to `single`, pagination will be by one month. <Calendar aria-label="Appointment date" visibleDuration={{ months: 3 }} pageBehavior="single" > <header> <Button slot="previous">◀</Button> <Heading /> <Button slot="next">▶</Button> </header> <div style={{ display: 'flex', gap: 30, overflow: 'auto' }}> <CalendarGrid> {(date) => <CalendarCell date={date} />} </CalendarGrid> <CalendarGrid offset={{ months: 1 }}> {(date) => <CalendarCell date={date} />} </CalendarGrid> <CalendarGrid offset={{ months: 2 }}> {(date) => <CalendarCell date={date} />} </CalendarGrid> </div> </Calendar> <Calendar aria-label="Appointment date" visibleDuration={{ months: 3 }} pageBehavior="single" > <header> <Button slot="previous">◀</Button> <Heading /> <Button slot="next">▶</Button> </header> <div style={{ display: 'flex', gap: 30, overflow: 'auto' }} > <CalendarGrid> {(date) => <CalendarCell date={date} />} </CalendarGrid> <CalendarGrid offset={{ months: 1 }}> {(date) => <CalendarCell date={date} />} </CalendarGrid> <CalendarGrid offset={{ months: 2 }}> {(date) => <CalendarCell date={date} />} </CalendarGrid> </div> </Calendar> <Calendar aria-label="Appointment date" visibleDuration={{ months: 3 }} pageBehavior="single" > <header> <Button slot="previous"> ◀ </Button> <Heading /> <Button slot="next"> ▶ </Button> </header> <div style={{ display: 'flex', gap: 30, overflow: 'auto' }} > <CalendarGrid> {(date) => ( <CalendarCell date={date} /> )} </CalendarGrid> <CalendarGrid offset={{ months: 1 }} > {(date) => ( <CalendarCell date={date} /> )} </CalendarGrid> <CalendarGrid offset={{ months: 2 }} > {(date) => ( <CalendarCell date={date} /> )} </CalendarGrid> </div> </Calendar> ## Appointment date, May to July 2025 ◀ ## May – July 2025 ▶ | S | M | T | W | T | F | S | | --- | --- | --- | --- | --- | --- | --- | | 27 | 28 | 29 | 30 | 1 | 2 | 3 | | 4 | 5 | 6 | 7 | 8 | 9 | 10 | | 11 | 12 | 13 | 14 | 15 | 16 | 17 | | 18 | 19 | 20 | 21 | 22 | 23 | 24 | | 25 | 26 | 27 | 28 | 29 | 30 | 31 | | S | M | T | W | T | F | S | | --- | --- | --- | --- | --- | --- | --- | | 1 | 2 | 3 | 4 | 5 | 6 | 7 | | 8 | 9 | 10 | 11 | 12 | 13 | 14 | | 15 | 16 | 17 | 18 | 19 | 20 | 21 | | 22 | 23 | 24 | 25 | 26 | 27 | 28 | | 29 | 30 | 1 | 2 | 3 | 4 | 5 | | S | M | T | W | T | F | S | | --- | --- | --- | --- | --- | --- | --- | | 29 | 30 | 1 | 2 | 3 | 4 | 5 | | 6 | 7 | 8 | 9 | 10 | 11 | 12 | | 13 | 14 | 15 | 16 | 17 | 18 | 19 | | 20 | 21 | 22 | 23 | 24 | 25 | 26 | | 27 | 28 | 29 | 30 | 31 | 1 | 2 | ## Validation# * * * By default, `Calendar` allows selecting any date. The `minValue` and `maxValue` props can also be used to prevent the user from selecting dates outside a certain range. This example only accepts dates after today. import {today} from '@internationalized/date'; <MyCalendar aria-label="Appointment date" minValue={today(getLocalTimeZone())} /> import {today} from '@internationalized/date'; <MyCalendar aria-label="Appointment date" minValue={today(getLocalTimeZone())} /> import {today} from '@internationalized/date'; <MyCalendar aria-label="Appointment date" minValue={today( getLocalTimeZone() )} /> ## Appointment date, June 2025 ◀ ## June 2025 ▶ | S | M | T | W | T | F | S | | --- | --- | --- | --- | --- | --- | --- | | 1 | 2 | 3 | 4 | 5 | 6 | 7 | | 8 | 9 | 10 | 11 | 12 | 13 | 14 | | 15 | 16 | 17 | 18 | 19 | 20 | 21 | | 22 | 23 | 24 | 25 | 26 | 27 | 28 | | 29 | 30 | 1 | 2 | 3 | 4 | 5 | Show CSS .react-aria-Calendar { .react-aria-CalendarCell { &[data-disabled] { color: var(--text-color-disabled); } } } .react-aria-Calendar { .react-aria-CalendarCell { &[data-disabled] { color: var(--text-color-disabled); } } } .react-aria-Calendar { .react-aria-CalendarCell { &[data-disabled] { color: var(--text-color-disabled); } } } ### Unavailable dates# `Calendar` supports marking certain dates as _unavailable_. These dates remain focusable with the keyboard so that navigation is consistent, but cannot be selected by the user. In this example, they are displayed in red. The `isDateUnavailable` prop accepts a callback that is called to evaluate whether each visible date is unavailable. This example includes multiple unavailable date ranges, e.g. dates when no appointments are available. In addition, all weekends are unavailable. The `minValue` prop is also used to prevent selecting dates before today. import {useLocale} from 'react-aria'; import {isWeekend, today} from '@internationalized/date'; function Example() { let now = today(getLocalTimeZone()); let disabledRanges = [ [now, now.add({ days: 5 })], [now.add({ days: 14 }), now.add({ days: 16 })], [now.add({ days: 23 }), now.add({ days: 24 })] ]; let { locale } = useLocale(); let isDateUnavailable = (date: DateValue) => isWeekend(date, locale) || disabledRanges.some((interval) => date.compare(interval[0]) >= 0 && date.compare(interval[1]) <= 0 ); return ( <MyCalendar aria-label="Appointment date" minValue={today(getLocalTimeZone())} isDateUnavailable={isDateUnavailable} /> ); } import {useLocale} from 'react-aria'; import {isWeekend, today} from '@internationalized/date'; function Example() { let now = today(getLocalTimeZone()); let disabledRanges = [ [now, now.add({ days: 5 })], [now.add({ days: 14 }), now.add({ days: 16 })], [now.add({ days: 23 }), now.add({ days: 24 })] ]; let { locale } = useLocale(); let isDateUnavailable = (date: DateValue) => isWeekend(date, locale) || disabledRanges.some((interval) => date.compare(interval[0]) >= 0 && date.compare(interval[1]) <= 0 ); return ( <MyCalendar aria-label="Appointment date" minValue={today(getLocalTimeZone())} isDateUnavailable={isDateUnavailable} /> ); } import {useLocale} from 'react-aria'; import { isWeekend, today } from '@internationalized/date'; function Example() { let now = today( getLocalTimeZone() ); let disabledRanges = [ [ now, now.add({ days: 5 }) ], [ now.add({ days: 14 }), now.add({ days: 16 }) ], [ now.add({ days: 23 }), now.add({ days: 24 }) ] ]; let { locale } = useLocale(); let isDateUnavailable = (date: DateValue) => isWeekend( date, locale ) || disabledRanges .some(( interval ) => date.compare( interval[0] ) >= 0 && date.compare( interval[1] ) <= 0 ); return ( <MyCalendar aria-label="Appointment date" minValue={today( getLocalTimeZone() )} isDateUnavailable={isDateUnavailable} /> ); } ## Appointment date, June 2025 ◀ ## June 2025 ▶ | S | M | T | W | T | F | S | | --- | --- | --- | --- | --- | --- | --- | | 1 | 2 | 3 | 4 | 5 | 6 | 7 | | 8 | 9 | 10 | 11 | 12 | 13 | 14 | | 15 | 16 | 17 | 18 | 19 | 20 | 21 | | 22 | 23 | 24 | 25 | 26 | 27 | 28 | | 29 | 30 | 1 | 2 | 3 | 4 | 5 | Show CSS .react-aria-Calendar { .react-aria-CalendarCell { &[data-unavailable] { text-decoration: line-through; color: var(--invalid-color); } } } .react-aria-Calendar { .react-aria-CalendarCell { &[data-unavailable] { text-decoration: line-through; color: var(--invalid-color); } } } .react-aria-Calendar { .react-aria-CalendarCell { &[data-unavailable] { text-decoration: line-through; color: var(--invalid-color); } } } ### Error message# `Calendar` tries to avoid allowing the user to select invalid dates in the first place (see Validation and Unavailable dates above). However, if according to application logic a selected date is invalid, the `isInvalid` prop can be set. This alerts assistive technology users that the selection is invalid, and can be used for styling purposes as well. In addition, the `errorMessage` slot may be used to help the user fix the issue. This example validates that the selected date is a weekday and not a weekend according to the current locale. import {today, isWeekend} from '@internationalized/date'; import {useLocale} from 'react-aria'; function Example() { let [date, setDate] = React.useState(today(getLocalTimeZone())); let {locale} = useLocale(); let isInvalid = isWeekend(date, locale); return ( <MyCalendar aria-label="Appointment date" value={date} onChange={setDate} isInvalid={isInvalid} errorMessage={isInvalid ? 'We are closed on weekends' : undefined} /> ); } import {isWeekend, today} from '@internationalized/date'; import {useLocale} from 'react-aria'; function Example() { let [date, setDate] = React.useState( today(getLocalTimeZone()) ); let { locale } = useLocale(); let isInvalid = isWeekend(date, locale); return ( <MyCalendar aria-label="Appointment date" value={date} onChange={setDate} isInvalid={isInvalid} errorMessage={isInvalid ? 'We are closed on weekends' : undefined} /> ); } import { isWeekend, today } from '@internationalized/date'; import {useLocale} from 'react-aria'; function Example() { let [date, setDate] = React.useState( today( getLocalTimeZone() ) ); let { locale } = useLocale(); let isInvalid = isWeekend( date, locale ); return ( <MyCalendar aria-label="Appointment date" value={date} onChange={setDate} isInvalid={isInvalid} errorMessage={isInvalid ? 'We are closed on weekends' : undefined} /> ); } ## Appointment date, June 2025 ◀ ## June 2025 ▶ | S | M | T | W | T | F | S | | --- | --- | --- | --- | --- | --- | --- | | 1 | 2 | 3 | 4 | 5 | 6 | 7 | | 8 | 9 | 10 | 11 | 12 | 13 | 14 | | 15 | 16 | 17 | 18 | 19 | 20 | 21 | | 22 | 23 | 24 | 25 | 26 | 27 | 28 | | 29 | 30 | 1 | 2 | 3 | 4 | 5 | Show CSS .react-aria-Calendar { .react-aria-CalendarCell { &[data-invalid] { background: var(--invalid-color); color: var(--highlight-foreground); } } [slot=errorMessage] { font-size: 12px; color: var(--invalid-color); } } .react-aria-Calendar { .react-aria-CalendarCell { &[data-invalid] { background: var(--invalid-color); color: var(--highlight-foreground); } } [slot=errorMessage] { font-size: 12px; color: var(--invalid-color); } } .react-aria-Calendar { .react-aria-CalendarCell { &[data-invalid] { background: var(--invalid-color); color: var(--highlight-foreground); } } [slot=errorMessage] { font-size: 12px; color: var(--invalid-color); } } ## Controlling the focused date# * * * By default, the selected date is focused when a `Calendar` first mounts. If no `value` or `defaultValue` prop is provided, then the current date is focused. However, `Calendar` supports controlling which date is focused using the `focusedValue` and `onFocusChange` props. This also determines which month is visible. The `defaultFocusedValue` prop allows setting the initial focused date when the `Calendar` first mounts, without controlling it. This example focuses July 1, 2021 by default. The user may change the focused date, and the `onFocusChange` event updates the state. Clicking the button resets the focused date back to the initial value. import {CalendarDate} from '@internationalized/date'; function Example() { let defaultDate = new CalendarDate(2021, 7, 1); let [focusedDate, setFocusedDate] = React.useState(defaultDate); return ( <> <button style={{ marginBottom: 20 }} onClick={() => setFocusedDate(defaultDate)} > Reset focused date </button> <MyCalendar focusedValue={focusedDate} onFocusChange={setFocusedDate} /> </> ); } import {CalendarDate} from '@internationalized/date'; function Example() { let defaultDate = new CalendarDate(2021, 7, 1); let [focusedDate, setFocusedDate] = React.useState( defaultDate ); return ( <> <button style={{ marginBottom: 20 }} onClick={() => setFocusedDate(defaultDate)} > Reset focused date </button> <MyCalendar focusedValue={focusedDate} onFocusChange={setFocusedDate} /> </> ); } import {CalendarDate} from '@internationalized/date'; function Example() { let defaultDate = new CalendarDate( 2021, 7, 1 ); let [ focusedDate, setFocusedDate ] = React.useState( defaultDate ); return ( <> <button style={{ marginBottom: 20 }} onClick={() => setFocusedDate( defaultDate )} > Reset focused date </button> <MyCalendar focusedValue={focusedDate} onFocusChange={setFocusedDate} /> </> ); } Reset focused date ## July 2021 ◀ ## July 2021 ▶ | S | M | T | W | T | F | S | | --- | --- | --- | --- | --- | --- | --- | | 27 | 28 | 29 | 30 | 1 | 2 | 3 | | 4 | 5 | 6 | 7 | 8 | 9 | 10 | | 11 | 12 | 13 | 14 | 15 | 16 | 17 | | 18 | 19 | 20 | 21 | 22 | 23 | 24 | | 25 | 26 | 27 | 28 | 29 | 30 | 31 | ## Disabled# * * * The `isDisabled` boolean prop makes the Calendar disabled. Cells cannot be focused or selected. <MyCalendar aria-label="Event date" isDisabled /> <MyCalendar aria-label="Event date" isDisabled /> <MyCalendar aria-label="Event date" isDisabled /> ## Event date, June 2025 ◀ ## June 2025 ▶ | S | M | T | W | T | F | S | | --- | --- | --- | --- | --- | --- | --- | | 1 | 2 | 3 | 4 | 5 | 6 | 7 | | 8 | 9 | 10 | 11 | 12 | 13 | 14 | | 15 | 16 | 17 | 18 | 19 | 20 | 21 | | 22 | 23 | 24 | 25 | 26 | 27 | 28 | | 29 | 30 | 1 | 2 | 3 | 4 | 5 | ### Read only# The `isReadOnly` boolean prop makes the Calendar's value immutable. Unlike `isDisabled`, the Calendar remains focusable. <MyCalendar aria-label="Event date" value={today(getLocalTimeZone())} isReadOnly /> <MyCalendar aria-label="Event date" value={today(getLocalTimeZone())} isReadOnly /> <MyCalendar aria-label="Event date" value={today( getLocalTimeZone() )} isReadOnly /> ## Event date, June 2025 ◀ ## June 2025 ▶ | S | M | T | W | T | F | S | | --- | --- | --- | --- | --- | --- | --- | | 1 | 2 | 3 | 4 | 5 | 6 | 7 | | 8 | 9 | 10 | 11 | 12 | 13 | 14 | | 15 | 16 | 17 | 18 | 19 | 20 | 21 | | 22 | 23 | 24 | 25 | 26 | 27 | 28 | | 29 | 30 | 1 | 2 | 3 | 4 | 5 | ## Custom first day of week# * * * By default, the first day of the week is automatically set based on the current locale. This can be changed by setting the `firstDayOfWeek` prop to `'sun'`, `'mon'`, `'tue'`, `'wed'`, `'thu'`, `'fri'`, or `'sat'`. <MyCalendar aria-label="Event date" value={today(getLocalTimeZone())} firstDayOfWeek="mon" /> <MyCalendar aria-label="Event date" value={today(getLocalTimeZone())} firstDayOfWeek="mon" /> <MyCalendar aria-label="Event date" value={today( getLocalTimeZone() )} firstDayOfWeek="mon" /> ## Event date, June 2025 ◀ ## June 2025 ▶ | M | T | W | T | F | S | S | | --- | --- | --- | --- | --- | --- | --- | | 26 | 27 | 28 | 29 | 30 | 31 | 1 | | 2 | 3 | 4 | 5 | 6 | 7 | 8 | | 9 | 10 | 11 | 12 | 13 | 14 | 15 | | 16 | 17 | 18 | 19 | 20 | 21 | 22 | | 23 | 24 | 25 | 26 | 27 | 28 | 29 | | 30 | 1 | 2 | 3 | 4 | 5 | 6 | ## Labeling# * * * An aria-label must be provided to the `Calendar` for accessibility. If it is labeled by a separate element, an `aria-labelledby` prop must be provided using the `id` of the labeling element instead. ### Internationalization# In order to internationalize a `Calendar`, a localized string should be passed to the `aria-label` prop. For languages that are read right-to-left (e.g. Hebrew and Arabic), keyboard navigation is automatically flipped. Ensure that your CSS accounts for this as well. Dates are automatically formatted using the current locale. ## Props# * * * ### Calendar# | Name | Type | Default | Description | | --- | --- | --- | --- | | `visibleDuration` | ` DateDuration ` | `{months: 1}` | The amount of days that will be displayed at once. This affects how pagination works. | | `createCalendar` | `( (identifier: CalendarIdentifier )) => Calendar ` | — | A function to create a new Calendar object for a given calendar identifier. If not provided, the `createCalendar` function from `@internationalized/date` will be used. | | `minValue` | ` DateValue | null` | — | The minimum allowed date that a user may select. | | `maxValue` | ` DateValue | null` | — | The maximum allowed date that a user may select. | | `isDateUnavailable` | `( (date: DateValue )) => boolean` | — | Callback that is called for each date of the calendar. If it returns true, then the date is unavailable. | | `isDisabled` | `boolean` | `false` | Whether the calendar is disabled. | | `isReadOnly` | `boolean` | `false` | Whether the calendar value is immutable. | | `autoFocus` | `boolean` | `false` | Whether to automatically focus the calendar when it mounts. | | `focusedValue` | ` DateValue | null` | — | Controls the currently focused date within the calendar. | | `defaultFocusedValue` | ` DateValue | null` | — | The date that is focused when the calendar first mounts (uncountrolled). | | `isInvalid` | `boolean` | — | Whether the current selection is invalid according to application logic. | | `pageBehavior` | ` PageBehavior ` | `visible` | Controls the behavior of paging. Pagination either works by advancing the visible page by visibleDuration (default) or one unit of visibleDuration. | | `firstDayOfWeek` | `'sun' | 'mon' | 'tue' | 'wed' | 'thu' | 'fri' | 'sat'` | — | The day that starts the week. | | `value` | ` DateValue | null` | — | The current value (controlled). | | `defaultValue` | ` DateValue | null` | — | The default value (uncontrolled). | | `children` | `ReactNode | ( (values: CalendarRenderProps & & { defaultChildren: ReactNode | | undefined } )) => ReactNode` | — | The children of the component. A function may be provided to alter the children based on component state. | | `className` | `string | ( (values: CalendarRenderProps & & { defaultClassName: string | | undefined } )) => string` | — | The CSS className for the element. A function may be provided to compute the class based on component state. | | `style` | `CSSProperties | ( (values: CalendarRenderProps & & { defaultStyle: CSSProperties } )) => CSSProperties | undefined` | — | The inline style for the element. A function may be provided to compute the style based on component state. | Events | Name | Type | Description | | --- | --- | --- | | `onFocusChange` | `( (date: CalendarDate )) => void` | Handler that is called when the focused date changes. | | `onChange` | `( (value: MappedDateValue < DateValue > )) => void` | Handler that is called when the value changes. | Layout | Name | Type | Description | | --- | --- | --- | | `slot` | `string | null` | A slot name for the component. Slots allow the component to receive props from a parent component. An explicit `null` value indicates that the local props completely override all props received from a parent. | Accessibility | Name | Type | Description | | --- | --- | --- | | `id` | `string` | The element's unique identifier. See MDN. | | `aria-label` | `string` | Defines a string value that labels the current element. | | `aria-labelledby` | `string` | Identifies the element (or elements) that labels the current element. | | `aria-describedby` | `string` | Identifies the element (or elements) that describes the object. | | `aria-details` | `string` | Identifies the element (or elements) that provide a detailed, extended description for the object. | ### Heading# A `<Heading>` accepts all HTML attributes. ### Button# A `<Button>` accepts its contents as `children`. Other props such as `onPress` and `isDisabled` will be set by the `Calendar`. Show props | Name | Type | Default | Description | | --- | --- | --- | --- | | `form` | `string` | — | The `<form>` element to associate the button with. The value of this attribute must be the id of a `<form>` in the same document. | | `formAction` | `string` | — | The URL that processes the information submitted by the button. Overrides the action attribute of the button's form owner. | | `formEncType` | `string` | — | Indicates how to encode the form data that is submitted. | | `formMethod` | `string` | — | Indicates the HTTP method used to submit the form. | | `formNoValidate` | `boolean` | — | Indicates that the form is not to be validated when it is submitted. | | `formTarget` | `string` | — | Overrides the target attribute of the button's form owner. | | `name` | `string` | — | Submitted as a pair with the button's value as part of the form data. | | `value` | `string` | — | The value associated with the button's name when it's submitted with the form data. | | `isPending` | `boolean` | — | Whether the button is in a pending state. This disables press and hover events while retaining focusability, and announces the pending state to screen readers. | | `isDisabled` | `boolean` | — | Whether the button is disabled. | | `autoFocus` | `boolean` | — | Whether the element should receive focus on render. | | `type` | `'button' | 'submit' | 'reset'` | `'button'` | The behavior of the button when used in an HTML form. | | `children` | `ReactNode | ( (values: ButtonRenderProps & & { defaultChildren: ReactNode | | undefined } )) => ReactNode` | — | The children of the component. A function may be provided to alter the children based on component state. | | `className` | `string | ( (values: ButtonRenderProps & & { defaultClassName: string | | undefined } )) => string` | — | The CSS className for the element. A function may be provided to compute the class based on component state. | | `style` | `CSSProperties | ( (values: ButtonRenderProps & & { defaultStyle: CSSProperties } )) => CSSProperties | undefined` | — | The inline style for the element. A function may be provided to compute the style based on component state. | Events | Name | Type | Description | | --- | --- | --- | | `onPress` | `( (e: PressEvent )) => void` | Handler that is called when the press is released over the target. | | `onPressStart` | `( (e: PressEvent )) => void` | Handler that is called when a press interaction starts. | | `onPressEnd` | `( (e: PressEvent )) => void` | Handler that is called when a press interaction ends, either over the target or when the pointer leaves the target. | | `onPressChange` | `( (isPressed: boolean )) => void` | Handler that is called when the press state changes. | | `onPressUp` | `( (e: PressEvent )) => void` | Handler that is called when a press is released over the target, regardless of whether it started on the target or not. | | `onClick` | `( (e: MouseEvent<FocusableElement> )) => void` | **Not recommended – use `onPress` instead.** `onClick` is an alias for `onPress` provided for compatibility with other libraries. `onPress` provides additional event details for non-mouse interactions. | | `onFocus` | `( (e: FocusEvent<Target> )) => void` | Handler that is called when the element receives focus. | | `onBlur` | `( (e: FocusEvent<Target> )) => void` | Handler that is called when the element loses focus. | | `onFocusChange` | `( (isFocused: boolean )) => void` | Handler that is called when the element's focus status changes. | | `onKeyDown` | `( (e: KeyboardEvent )) => void` | Handler that is called when a key is pressed. | | `onKeyUp` | `( (e: KeyboardEvent )) => void` | Handler that is called when a key is released. | | `onHoverStart` | `( (e: HoverEvent )) => void` | Handler that is called when a hover interaction starts. | | `onHoverEnd` | `( (e: HoverEvent )) => void` | Handler that is called when a hover interaction ends. | | `onHoverChange` | `( (isHovering: boolean )) => void` | Handler that is called when the hover state changes. | Layout | Name | Type | Description | | --- | --- | --- | | `slot` | `string | null` | A slot name for the component. Slots allow the component to receive props from a parent component. An explicit `null` value indicates that the local props completely override all props received from a parent. | Accessibility | Name | Type | Description | | --- | --- | --- | | `id` | `string` | The element's unique identifier. See MDN. | | `excludeFromTabOrder` | `boolean` | Whether to exclude the element from the sequential tab order. If true, the element will not be focusable via the keyboard by tabbing. This should be avoided except in rare scenarios where an alternative means of accessing the element or its functionality via the keyboard is available. | | `preventFocusOnPress` | `boolean` | Whether to prevent focus from moving to the button when pressing it. Caution, this can make the button inaccessible and should only be used when alternative keyboard interaction is provided, such as ComboBox's MenuTrigger or a NumberField's increment/decrement control. | | `aria-expanded` | `boolean | 'true' | 'false'` | Indicates whether the element, or another grouping element it controls, is currently expanded or collapsed. | | `aria-haspopup` | `boolean | 'menu' | 'listbox' | 'tree' | 'grid' | 'dialog' | 'true' | 'false'` | Indicates the availability and type of interactive popup element, such as menu or dialog, that can be triggered by an element. | | `aria-controls` | `string` | Identifies the element (or elements) whose contents or presence are controlled by the current element. | | `aria-pressed` | `boolean | 'true' | 'false' | 'mixed'` | Indicates the current "pressed" state of toggle buttons. | | `aria-current` | `boolean | 'true' | 'false' | 'page' | 'step' | 'location' | 'date' | 'time'` | Indicates whether this element represents the current item within a container or set of related elements. | | `aria-label` | `string` | Defines a string value that labels the current element. | | `aria-labelledby` | `string` | Identifies the element (or elements) that labels the current element. | | `aria-describedby` | `string` | Identifies the element (or elements) that describes the object. | | `aria-details` | `string` | Identifies the element (or elements) that provide a detailed, extended description for the object. | ### CalendarGrid# A `<CalendarGrid>` renders an individual month within a `<Calendar>`. It accepts a function as its `children`, which is called to render a `<CalendarCell>` for each date. This renders a default `<CalendarGridHeader>`, which displays the weekday names in the column headers. This can be customized by providing a `<CalendarGridHeader>` and `<CalendarGridBody>` as children instead of a function. | Name | Type | Default | Description | | --- | --- | --- | --- | | `children` | `ReactElement | ReactElement[] | ( (date: CalendarDate )) => ReactElement` | — | Either a function to render calendar cells for each date in the month, or children containing a `<CalendarGridHeader>`` and`<CalendarGridBody>\` when additional customization is needed. | | `offset` | ` DateDuration ` | — | An offset from the beginning of the visible date range that this CalendarGrid should display. Useful when displaying more than one month at a time. | | `weekdayStyle` | `'narrow' | 'short' | 'long'` | `"narrow"` | The style of weekday names to display in the calendar grid header, e.g. single letter, abbreviation, or full day name. | | `className` | `string` | — | The CSS className for the element. | | `style` | `CSSProperties` | — | The inline style for the element. | ### CalendarGridHeader# A `<CalendarGridHeader>` renders the header within a `<CalendarGrid>`, displaying a list of weekday names. It accepts a function as its `children`, which is called with a day name abbreviation to render a `<CalendarHeaderCell>` for each column header. Show props | Name | Type | Description | | --- | --- | --- | | `children` | `( (day: string )) => ReactElement` | A function to render a `<CalendarHeaderCell>` for a weekday name. | | `className` | `string` | The CSS className for the element. | | `style` | `CSSProperties` | The inline style for the element. | ### CalendarHeaderCell# A `<CalendarHeaderCell>` renders a column header within a `<CalendarGridHeader>`. It typically displays a weekday name. Show props | Name | Type | Description | | --- | --- | --- | | `children` | `ReactNode` | The children of the component. | | `className` | `string` | The CSS className for the element. | | `style` | `CSSProperties` | The inline style for the element. | Accessibility | Name | Type | Description | | --- | --- | --- | | `id` | `string` | The element's unique identifier. See MDN. | ### CalendarGridBody# A `<CalendarGridBody>` renders the body within a `<CalendarGrid>`. It accepts a function as its `children`, which is called to render a `<CalendarCell>` for each date. Show props | Name | Type | Description | | --- | --- | --- | | `children` | `( (date: CalendarDate )) => ReactElement` | A function to render a `<CalendarCell>` for a given date. | | `className` | `string` | The CSS className for the element. | | `style` | `CSSProperties` | The inline style for the element. | ### CalendarCell# A `<CalendarCell>` renders an individual date within a `<CalendarGrid>`. Show props | Name | Type | Description | | --- | --- | --- | | `date` | ` CalendarDate ` | The date to render in the cell. | | `children` | `ReactNode | ( (values: CalendarCellRenderProps & & { defaultChildren: ReactNode | | undefined } )) => ReactNode` | The children of the component. A function may be provided to alter the children based on component state. | | `className` | `string | ( (values: CalendarCellRenderProps & & { defaultClassName: string | | undefined } )) => string` | The CSS className for the element. A function may be provided to compute the class based on component state. | | `style` | `CSSProperties | ( (values: CalendarCellRenderProps & & { defaultStyle: CSSProperties } )) => CSSProperties | undefined` | The inline style for the element. A function may be provided to compute the style based on component state. | Events | Name | Type | Description | | --- | --- | --- | | `onHoverStart` | `( (e: HoverEvent )) => void` | Handler that is called when a hover interaction starts. | | `onHoverEnd` | `( (e: HoverEvent )) => void` | Handler that is called when a hover interaction ends. | | `onHoverChange` | `( (isHovering: boolean )) => void` | Handler that is called when the hover state changes. | ## Styling# * * * React Aria components can be styled in many ways, including using CSS classes, inline styles, utility classes (e.g. Tailwind), CSS-in-JS (e.g. Styled Components), etc. By default, all components include a builtin `className` attribute which can be targeted using CSS selectors. These follow the `react-aria-ComponentName` naming convention. .react-aria-Calendar { /* ... */ } .react-aria-Calendar { /* ... */ } .react-aria-Calendar { /* ... */ } A custom `className` can also be specified on any component. This overrides the default `className` provided by React Aria with your own. <CalendarGrid className="my-calendar-grid"> {/* ... */} </CalendarGrid> <CalendarGrid className="my-calendar-grid"> {/* ... */} </CalendarGrid> <CalendarGrid className="my-calendar-grid"> {/* ... */} </CalendarGrid> In addition, some components support multiple UI states (e.g. focused, placeholder, readonly, etc.). React Aria components expose states using data attributes, which you can target in CSS selectors. For example: .react-aria-CalendarCell[data-selected] { /* ... */ } .react-aria-CalendarCell[data-invalid] { /* ... */ } .react-aria-CalendarCell[data-selected] { /* ... */ } .react-aria-CalendarCell[data-invalid] { /* ... */ } .react-aria-CalendarCell[data-selected] { /* ... */ } .react-aria-CalendarCell[data-invalid] { /* ... */ } The `className` and `style` props also accept functions which receive states for styling. This lets you dynamically determine the classes or styles to apply, which is useful when using utility CSS libraries like Tailwind. <CalendarCell className={({ isSelected }) => isSelected ? 'bg-blue-600' : 'bg-gray-600'} /> <CalendarCell className={({ isSelected }) => isSelected ? 'bg-blue-600' : 'bg-gray-600'} /> <CalendarCell className={( { isSelected } ) => isSelected ? 'bg-blue-600' : 'bg-gray-600'} /> Render props may also be used as children to alter what elements are rendered based on the current state. For example, you could add an additional element when a date is unavailable. <CalendarCell> {({formattedDate, isUnavailable}) => ( <> {isUnavailable && <UnavailableIcon />} <span>{formattedDate}</span> </> )} </CalendarCell> <CalendarCell> {({formattedDate, isUnavailable}) => ( <> {isUnavailable && <UnavailableIcon />} <span>{formattedDate}</span> </> )} </CalendarCell> <CalendarCell> {( { formattedDate, isUnavailable } ) => ( <> {isUnavailable && ( <UnavailableIcon /> )} <span> {formattedDate} </span> </> )} </CalendarCell> The states, selectors, and render props for each component used in a `Calendar` are documented below. ### Calendar# A `Calendar` can be targeted with the `.react-aria-Calendar` CSS selector, or by overriding with a custom `className`. It supports the following states and render props: | Name | CSS Selector | Description | | --- | --- | --- | | `isDisabled` | `[data-disabled]` | Whether the calendar is disabled. | | `state` | `—` | State of the calendar. | | `isInvalid` | `[data-invalid]` | Whether the calendar is invalid. | ### Button# A Button can be targeted with the `.react-aria-Button` CSS selector, or by overriding with a custom `className`. The next and previous buttons can be targeted specifically with the `[slot=previous]` and `[slot=next]` selectors. Buttons support the following states: | Name | CSS Selector | Description | | --- | --- | --- | | `isHovered` | `[data-hovered]` | Whether the button is currently hovered with a mouse. | | `isPressed` | `[data-pressed]` | Whether the button is currently in a pressed state. | | `isFocused` | `[data-focused]` | Whether the button is focused, either via a mouse or keyboard. | | `isFocusVisible` | `[data-focus-visible]` | Whether the button is keyboard focused. | | `isDisabled` | `[data-disabled]` | Whether the button is disabled. | | `isPending` | `[data-pending]` | Whether the button is currently in a pending state. | ### CalendarGrid# A `CalendarGrid` can be targeted with the `.react-aria-CalendarGrid` CSS selector, or by overriding with a custom `className`. When a function is provided as children, a default `<CalendarGridHeader>` and `<CalendarGridBody>` are rendered. If you need to customize the styling of the header cells, you can render them yourself. import {CalendarGridBody, CalendarGridHeader, CalendarHeaderCell} from 'react-aria-components'; <Calendar aria-label="Appointment date"> <header> <Button slot="previous">◀</Button> <Heading /> <Button slot="next">▶</Button> </header> <CalendarGrid> <CalendarGridHeader> {(day) => ( <CalendarHeaderCell style={{ color: 'var(--blue)' }}> {day} </CalendarHeaderCell> )} </CalendarGridHeader> <CalendarGridBody> {(date) => <CalendarCell date={date} />} </CalendarGridBody> </CalendarGrid> </Calendar> import { CalendarGridBody, CalendarGridHeader, CalendarHeaderCell } from 'react-aria-components'; <Calendar aria-label="Appointment date"> <header> <Button slot="previous">◀</Button> <Heading /> <Button slot="next">▶</Button> </header> <CalendarGrid> <CalendarGridHeader> {(day) => ( <CalendarHeaderCell style={{ color: 'var(--blue)' }} > {day} </CalendarHeaderCell> )} </CalendarGridHeader> <CalendarGridBody> {(date) => <CalendarCell date={date} />} </CalendarGridBody> </CalendarGrid> </Calendar> import { CalendarGridBody, CalendarGridHeader, CalendarHeaderCell } from 'react-aria-components'; <Calendar aria-label="Appointment date"> <header> <Button slot="previous"> ◀ </Button> <Heading /> <Button slot="next"> ▶ </Button> </header> <CalendarGrid> <CalendarGridHeader> {(day) => ( <CalendarHeaderCell style={{ color: 'var(--blue)' }} > {day} </CalendarHeaderCell> )} </CalendarGridHeader> <CalendarGridBody> {(date) => ( <CalendarCell date={date} /> )} </CalendarGridBody> </CalendarGrid> </Calendar> ## Appointment date, June 2025 ◀ ## June 2025 ▶ | S | M | T | W | T | F | S | | --- | --- | --- | --- | --- | --- | --- | | 1 | 2 | 3 | 4 | 5 | 6 | 7 | | 8 | 9 | 10 | 11 | 12 | 13 | 14 | | 15 | 16 | 17 | 18 | 19 | 20 | 21 | | 22 | 23 | 24 | 25 | 26 | 27 | 28 | | 29 | 30 | 1 | 2 | 3 | 4 | 5 | ### CalendarGridHeader# A `CalendarGridHeader` can be targeted with the `.react-aria-CalendarGridHeader` CSS selector, or by overriding with a custom `className`. ### CalendarHeaderCell# A `CalendarHeaderCell` can be targeted with the `.react-aria-CalendarHeaderCell` CSS selector, or by overriding with a custom `className`. ### CalendarGridBody# A `CalendarGridBody` can be targeted with the `.react-aria-CalendarGridBody` CSS selector, or by overriding with a custom `className`. ### CalendarCell# A `CalendarCell` can be targeted with the `.react-aria-CalendarCell` CSS selector, or by overriding with a custom `className`. It supports the following states: | Name | CSS Selector | Description | | --- | --- | --- | | `date` | `—` | The date that the cell represents. | | `formattedDate` | `—` | The day number formatted according to the current locale. | | `isHovered` | `[data-hovered]` | Whether the cell is currently hovered with a mouse. | | `isPressed` | `[data-pressed]` | Whether the cell is currently being pressed. | | `isSelected` | `[data-selected]` | Whether the cell is selected. | | `isSelectionStart` | `[data-selection-start]` | Whether the cell is the first date in a range selection. | | `isSelectionEnd` | `[data-selection-end]` | Whether the cell is the last date in a range selection. | | `isFocused` | `[data-focused]` | Whether the cell is focused. | | `isFocusVisible` | `[data-focus-visible]` | Whether the cell is keyboard focused. | | `isDisabled` | `[data-disabled]` | Whether the cell is disabled, according to the calendar's `minValue`, `maxValue`, and `isDisabled` props. Disabled dates are not focusable, and cannot be selected by the user. They are typically displayed with a dimmed appearance. | | `isOutsideVisibleRange` | `[data-outside-visible-range]` | Whether the cell is outside the visible range of the calendar. For example, dates before the first day of a month in the same week. | | `isOutsideMonth` | `[data-outside-month]` | Whether the cell is outside the current month. | | `isUnavailable` | `[data-unavailable]` | Whether the cell is unavailable, according to the calendar's `isDateUnavailable` prop. Unavailable dates remain focusable, but cannot be selected by the user. They should be displayed with a visual affordance to indicate they are unavailable, such as a different color or a strikethrough. Note that because they are focusable, unavailable dates must meet a 4.5:1 color contrast ratio, as defined by WCAG. | | `isInvalid` | `[data-invalid]` | Whether the cell is part of an invalid selection. | ### Text# The error message element within a `Calendar` can be targeted with the `[slot=errorMessage]` CSS selector, or by adding a custom `className`. ## Advanced customization# * * * ### Composition# If you need to customize one of the components within a `Calendar`, such as `CalendarGrid` or `CalendarCell`, in many cases you can create a wrapper component. This lets you customize the props passed to the component. function MyCalendarCell(props) { return <CalendarCell {...props} className="my-item" /> } function MyCalendarCell(props) { return <CalendarCell {...props} className="my-item" /> } function MyCalendarCell( props ) { return ( <CalendarCell {...props} className="my-item" /> ); } ### Contexts# All React Aria Components export a corresponding context that can be used to send props to them from a parent element. This enables you to build your own compositional APIs similar to those found in React Aria Components itself. You can send any prop or ref via context that you could pass to the corresponding component. The local props and ref on the component are merged with the ones passed via context, with the local props taking precedence (following the rules documented in mergeProps). | Component | Context | Props | Ref | | --- | --- | --- | --- | | `Calendar` | `CalendarContext` | ` CalendarProps ` | `HTMLDivElement` | This example uses `CalendarContext` to create a composite component containing a calendar and buttons representing preset dates. The `useSlottedContext` hook can be used to consume contexts that support the `slot` prop. import {CalendarContext, useSlottedContext} from 'react-aria-components'; function CalendarPicker({ children }) { let [value, onChange] = React.useState(null); let [focusedValue, onFocusChange] = React.useState(null); return ( <CalendarContext.Provider value={{ value, onChange, focusedValue, onFocusChange }} > <div className="calendar-picker"> {children} </div> </CalendarContext.Provider> ); } interface PresetProps { date: CalendarDate; children: React.ReactNode; } function Preset({ date, children }: PresetProps) { let context = useSlottedContext(CalendarContext)!; let onPress = () => { context.onFocusChange(date); context.onChange(date); }; return <Button onPress={onPress}>{children}</Button>; } import { CalendarContext, useSlottedContext } from 'react-aria-components'; function CalendarPicker({ children }) { let [value, onChange] = React.useState(null); let [focusedValue, onFocusChange] = React.useState(null); return ( <CalendarContext.Provider value={{ value, onChange, focusedValue, onFocusChange }} > <div className="calendar-picker"> {children} </div> </CalendarContext.Provider> ); } interface PresetProps { date: CalendarDate; children: React.ReactNode; } function Preset({ date, children }: PresetProps) { let context = useSlottedContext(CalendarContext)!; let onPress = () => { context.onFocusChange(date); context.onChange(date); }; return <Button onPress={onPress}>{children}</Button>; } import { CalendarContext, useSlottedContext } from 'react-aria-components'; function CalendarPicker( { children } ) { let [value, onChange] = React.useState(null); let [ focusedValue, onFocusChange ] = React.useState( null ); return ( <CalendarContext.Provider value={{ value, onChange, focusedValue, onFocusChange }} > <div className="calendar-picker"> {children} </div> </CalendarContext.Provider> ); } interface PresetProps { date: CalendarDate; children: React.ReactNode; } function Preset( { date, children }: PresetProps ) { let context = useSlottedContext( CalendarContext )!; let onPress = () => { context .onFocusChange( date ); context.onChange( date ); }; return ( <Button onPress={onPress} > {children} </Button> ); } Now you can combine a `Calendar` and one or more `Preset` components in a `CalendarPicker`. import {startOfWeek, startOfMonth} from '@internationalized/date'; import {useLocale} from 'react-aria'; function Example() { let {locale} = useLocale(); let now = today(getLocalTimeZone()); return ( <CalendarPicker> <Preset date={now}>Today</Preset> <Preset date={startOfWeek(now.add({weeks: 1}), locale)}>Next week</Preset> <Preset date={startOfMonth(now.add({months: 1}))}>Next month</Preset> <MyCalendar aria-label="Meeting date" /> </CalendarPicker> ); } import { startOfMonth, startOfWeek } from '@internationalized/date'; import {useLocale} from 'react-aria'; function Example() { let { locale } = useLocale(); let now = today(getLocalTimeZone()); return ( <CalendarPicker> <Preset date={now}>Today</Preset> <Preset date={startOfWeek(now.add({ weeks: 1 }), locale)} > Next week </Preset> <Preset date={startOfMonth(now.add({ months: 1 }))}> Next month </Preset> <MyCalendar aria-label="Meeting date" /> </CalendarPicker> ); } import { startOfMonth, startOfWeek } from '@internationalized/date'; import {useLocale} from 'react-aria'; function Example() { let { locale } = useLocale(); let now = today( getLocalTimeZone() ); return ( <CalendarPicker> <Preset date={now}> Today </Preset> <Preset date={startOfWeek( now.add({ weeks: 1 }), locale )} > Next week </Preset> <Preset date={startOfMonth( now.add({ months: 1 }) )} > Next month </Preset> <MyCalendar aria-label="Meeting date" /> </CalendarPicker> ); } TodayNext weekNext month ## Meeting date, June 2025 ◀ ## June 2025 ▶ | S | M | T | W | T | F | S | | --- | --- | --- | --- | --- | --- | --- | | 1 | 2 | 3 | 4 | 5 | 6 | 7 | | 8 | 9 | 10 | 11 | 12 | 13 | 14 | | 15 | 16 | 17 | 18 | 19 | 20 | 21 | | 22 | 23 | 24 | 25 | 26 | 27 | 28 | | 29 | 30 | 1 | 2 | 3 | 4 | 5 | Show CSS .calendar-picker { > .react-aria-Button { margin: 0 4px 8px 4px; } } .calendar-picker { > .react-aria-Button { margin: 0 4px 8px 4px; } } .calendar-picker { > .react-aria-Button { margin: 0 4px 8px 4px; } } ### Custom children# Calendar passes props to its child components, such as the heading and buttons, via their associated contexts. These contexts are exported so you can also consume them in your own custom components. This enables you to reuse existing components from your app or component library together with React Aria Components. | Component | Context | Props | Ref | | --- | --- | --- | --- | | `Heading` | `HeadingContext` | ` HeadingProps ` | `HTMLHeadingElement` | | `Button` | `ButtonContext` | ` ButtonProps ` | `HTMLButtonElement` | | `Text` | `TextContext` | ` TextProps ` | `HTMLElement` | This example consumes from `HeadingContext` in an existing styled heading component to make it compatible with React Aria Components. The `useContextProps` hook merges the local props and ref with the ones provided via context by Calendar. import type {HeadingProps} from 'react-aria-components'; import {HeadingContext, useContextProps} from 'react-aria-components'; const MyCustomHeading = React.forwardRef( (props: HeadingProps, ref: React.ForwardedRef<HTMLHeadingElement>) => { // Merge the local props and ref with the ones provided via context. [props, ref] = useContextProps(props, ref, HeadingContext); // ... your existing Heading component return <h2 {...props} ref={ref} />; } ); import type {HeadingProps} from 'react-aria-components'; import { HeadingContext, useContextProps } from 'react-aria-components'; const MyCustomHeading = React.forwardRef( ( props: HeadingProps, ref: React.ForwardedRef<HTMLHeadingElement> ) => { // Merge the local props and ref with the ones provided via context. [props, ref] = useContextProps( props, ref, HeadingContext ); // ... your existing Heading component return <h2 {...props} ref={ref} />; } ); import type {HeadingProps} from 'react-aria-components'; import { HeadingContext, useContextProps } from 'react-aria-components'; const MyCustomHeading = React.forwardRef( ( props: HeadingProps, ref: React.ForwardedRef< HTMLHeadingElement > ) => { // Merge the local props and ref with the ones provided via context. [props, ref] = useContextProps( props, ref, HeadingContext ); // ... your existing Heading component return ( <h2 {...props} ref={ref} /> ); } ); Now you can use `MyCustomHeading` within a `Calendar`, in place of the builtin React Aria Components `Heading`. <Calendar> <MyCustomHeading /> {/* ... */} </Calendar> <Calendar> <MyCustomHeading /> {/* ... */} </Calendar> <Calendar> <MyCustomHeading /> {/* ... */} </Calendar> ### State# Calendar provides a `CalendarState` object to its children via `CalendarStateContext`. This can be used to access and manipulate the calendar's state. This example shows a `CalendarValue` component that can be placed within a `Calendar` to display the currently selected date as a formatted string. import {CalendarStateContext} from 'react-aria-components'; import {useDateFormatter} from 'react-aria'; function CalendarValue() { let state = React.useContext(CalendarStateContext)!; let date = state.value?.toDate(getLocalTimeZone()); let {format} = useDateFormatter(); let formatted = date ? format(date) : 'None'; return <small>Selected date: {formatted}</small>; } <Calendar> <header> <Button slot="previous">◀</Button> <Heading /> <Button slot="next">▶</Button> </header> <CalendarGrid> {date => <CalendarCell date={date} />} </CalendarGrid> <CalendarValue /></Calendar> import {CalendarStateContext} from 'react-aria-components'; import {useDateFormatter} from 'react-aria'; function CalendarValue() { let state = React.useContext(CalendarStateContext)!; let date = state.value?.toDate(getLocalTimeZone()); let {format} = useDateFormatter(); let formatted = date ? format(date) : 'None'; return <small>Selected date: {formatted}</small>; } <Calendar> <header> <Button slot="previous">◀</Button> <Heading /> <Button slot="next">▶</Button> </header> <CalendarGrid> {date => <CalendarCell date={date} />} </CalendarGrid> <CalendarValue /></Calendar> import {CalendarStateContext} from 'react-aria-components'; import {useDateFormatter} from 'react-aria'; function CalendarValue() { let state = React .useContext( CalendarStateContext )!; let date = state.value ?.toDate( getLocalTimeZone() ); let { format } = useDateFormatter(); let formatted = date ? format(date) : 'None'; return ( <small> Selected date:{' '} {formatted} </small> ); } <Calendar> <header> <Button slot="previous"> ◀ </Button> <Heading /> <Button slot="next"> ▶ </Button> </header> <CalendarGrid> {(date) => ( <CalendarCell date={date} /> )} </CalendarGrid> <CalendarValue /></Calendar> ## June 2025 ◀ ## June 2025 ▶ | S | M | T | W | T | F | S | | --- | --- | --- | --- | --- | --- | --- | | 1 | 2 | 3 | 4 | 5 | 6 | 7 | | 8 | 9 | 10 | 11 | 12 | 13 | 14 | | 15 | 16 | 17 | 18 | 19 | 20 | 21 | | 22 | 23 | 24 | 25 | 26 | 27 | 28 | | 29 | 30 | 1 | 2 | 3 | 4 | 5 | Selected date: None ### Hooks# If you need to customize things even further, such as accessing internal state or customizing DOM structure, you can drop down to the lower level Hook-based API. See useCalendar for more details. This example uses the `useCalendarGrid` hook to build a single week calendar view. import type {CalendarGridProps} from 'react-aria-components'; import {CalendarStateContext} from 'react-aria-components'; import {useCalendarGrid} from 'react-aria'; function WeekCalendarGrid(props: CalendarGridProps) { let state = React.useContext(CalendarStateContext)!; let { gridProps } = useCalendarGrid(props, state); return ( <table {...gridProps}> <tbody> <tr> {state.getDatesInWeek(0).map((date, i) => ( <CalendarCell key={i} date={date} /> ))} </tr> </tbody> </table> ); } import type {CalendarGridProps} from 'react-aria-components'; import {CalendarStateContext} from 'react-aria-components'; import {useCalendarGrid} from 'react-aria'; function WeekCalendarGrid(props: CalendarGridProps) { let state = React.useContext(CalendarStateContext)!; let { gridProps } = useCalendarGrid(props, state); return ( <table {...gridProps}> <tbody> <tr> {state.getDatesInWeek(0).map((date, i) => ( <CalendarCell key={i} date={date} /> ))} </tr> </tbody> </table> ); } import type {CalendarGridProps} from 'react-aria-components'; import {CalendarStateContext} from 'react-aria-components'; import {useCalendarGrid} from 'react-aria'; function WeekCalendarGrid( props: CalendarGridProps ) { let state = React .useContext( CalendarStateContext )!; let { gridProps } = useCalendarGrid( props, state ); return ( <table {...gridProps} > <tbody> <tr> {state .getDatesInWeek( 0 ).map(( date, i ) => ( <CalendarCell key={i} date={date} /> ))} </tr> </tbody> </table> ); } `WeekCalendarGrid` can be used within a `Calendar` in place of the default `CalendarGrid` component. <Calendar visibleDuration={{ weeks: 1 }} defaultValue={today(getLocalTimeZone())} > <div className="week"> <Heading /> <Button slot="previous">◀</Button> <WeekCalendarGrid /> <Button slot="next">▶</Button> </div> </Calendar> <Calendar visibleDuration={{ weeks: 1 }} defaultValue={today(getLocalTimeZone())} > <div className="week"> <Heading /> <Button slot="previous">◀</Button> <WeekCalendarGrid /> <Button slot="next">▶</Button> </div> </Calendar> <Calendar visibleDuration={{ weeks: 1 }} defaultValue={today( getLocalTimeZone() )} > <div className="week"> <Heading /> <Button slot="previous"> ◀ </Button> <WeekCalendarGrid /> <Button slot="next"> ▶ </Button> </div> </Calendar> ## June 15 to 21, 2025 ## June 15 – 21, 2025 ◀ <table id="react-aria7761198108-«r6n»" role="grid"><tbody><tr><td role="gridcell"><div data-react-="" tabindex="-1" role="button" data-rac="">15</div></td><td role="gridcell"><div data-react-="" tabindex="-1" role="button" data-rac="">16</div></td><td role="gridcell"><div data-react-="" tabindex="-1" role="button" data-rac="">17</div></td><td role="gridcell"><div data-react-="" tabindex="0" role="button" data-selected="true" data-rac="">18</div></td><td role="gridcell"><div data-react-="" tabindex="-1" role="button" data-rac="">19</div></td><td role="gridcell"><div data-react-="" tabindex="-1" role="button" data-rac="">20</div></td><td role="gridcell"><div data-react-="" tabindex="-1" role="button" data-rac="">21</div></td></tr></tbody></table> ▶ Show CSS .week { display: grid; grid-template-areas: "heading heading heading" "previous grid next"; align-items: center; justify-items: center; gap: 8px; .react-aria-Heading { grid-area: heading; margin: 0; font-size: 1.2rem; } .react-aria-CalendarCell[data-outside-month] { display: block; } } .week { display: grid; grid-template-areas: "heading heading heading" "previous grid next"; align-items: center; justify-items: center; gap: 8px; .react-aria-Heading { grid-area: heading; margin: 0; font-size: 1.2rem; } .react-aria-CalendarCell[data-outside-month] { display: block; } } .week { display: grid; grid-template-areas: "heading heading heading" "previous grid next"; align-items: center; justify-items: center; gap: 8px; .react-aria-Heading { grid-area: heading; margin: 0; font-size: 1.2rem; } .react-aria-CalendarCell[data-outside-month] { display: block; } } --- ## Page: https://react-spectrum.adobe.com/react-aria/DateField.html # DateField A date field allows users to enter and edit date and time values using a keyboard. Each part of a date value is displayed in an individually editable segment. <table><tbody><tr><th>install</th><td><code>yarn add react-aria-components</code></td></tr><tr><th>version</th><td>1.10.1</td></tr><tr><th>usage</th><td><code><span>import</span> {DateField} <span>from</span> <span>'react-aria-components'</span></code></td></tr></tbody></table> View repository GitHub View package NPM ## Example# * * * import {DateField, Label, DateInput, DateSegment} from 'react-aria-components'; <DateField> <Label>Birth date</Label> <DateInput> {segment => <DateSegment segment={segment} />} </DateInput> </DateField> import { DateField, DateInput, DateSegment, Label } from 'react-aria-components'; <DateField> <Label>Birth date</Label> <DateInput> {(segment) => <DateSegment segment={segment} />} </DateInput> </DateField> import { DateField, DateInput, DateSegment, Label } from 'react-aria-components'; <DateField> <Label> Birth date </Label> <DateInput> {(segment) => ( <DateSegment segment={segment} /> )} </DateInput> </DateField> Birth date mm/dd/yyyy Show CSS @import "@react-aria/example-theme"; .react-aria-DateField { color: var(--text-color); display: flex; flex-direction: column; } .react-aria-DateInput { display: inline; padding: 4px; border: 1px solid var(--border-color); border-radius: 6px; background: var(--field-background); width: fit-content; min-width: 150px; white-space: nowrap; forced-color-adjust: none; &[data-focus-within] { outline: 2px solid var(--focus-ring-color); outline-offset: -1px; } } .react-aria-DateSegment { padding: 0 2px; font-variant-numeric: tabular-nums; text-align: end; color: var(--text-color); &[data-type=literal] { padding: 0; } &[data-placeholder] { color: var(--text-color-placeholder); font-style: italic; } &:focus { color: var(--highlight-foreground); background: var(--highlight-background); outline: none; border-radius: 4px; caret-color: transparent; } } @import "@react-aria/example-theme"; .react-aria-DateField { color: var(--text-color); display: flex; flex-direction: column; } .react-aria-DateInput { display: inline; padding: 4px; border: 1px solid var(--border-color); border-radius: 6px; background: var(--field-background); width: fit-content; min-width: 150px; white-space: nowrap; forced-color-adjust: none; &[data-focus-within] { outline: 2px solid var(--focus-ring-color); outline-offset: -1px; } } .react-aria-DateSegment { padding: 0 2px; font-variant-numeric: tabular-nums; text-align: end; color: var(--text-color); &[data-type=literal] { padding: 0; } &[data-placeholder] { color: var(--text-color-placeholder); font-style: italic; } &:focus { color: var(--highlight-foreground); background: var(--highlight-background); outline: none; border-radius: 4px; caret-color: transparent; } } @import "@react-aria/example-theme"; .react-aria-DateField { color: var(--text-color); display: flex; flex-direction: column; } .react-aria-DateInput { display: inline; padding: 4px; border: 1px solid var(--border-color); border-radius: 6px; background: var(--field-background); width: fit-content; min-width: 150px; white-space: nowrap; forced-color-adjust: none; &[data-focus-within] { outline: 2px solid var(--focus-ring-color); outline-offset: -1px; } } .react-aria-DateSegment { padding: 0 2px; font-variant-numeric: tabular-nums; text-align: end; color: var(--text-color); &[data-type=literal] { padding: 0; } &[data-placeholder] { color: var(--text-color-placeholder); font-style: italic; } &:focus { color: var(--highlight-foreground); background: var(--highlight-background); outline: none; border-radius: 4px; caret-color: transparent; } } ## Features# * * * A date field can be built using `<input type="date">`, but this is very limited in functionality, lacking in internationalization capabilities, inconsistent between browsers, and difficult to style. `DateField` helps achieve accessible and international date and time fields that can be styled as needed. * **Dates and times** – Support for dates and times with configurable granularity. * **International** – Support for 13 calendar systems used around the world, including Gregorian, Buddhist, Islamic, Persian, and more. Locale-specific formatting, number systems, hour cycles, and right-to-left support are available as well. * **Time zone aware** – Dates and times can optionally include a time zone. All modifications follow time zone rules such as daylight saving time. * **Accessible** – Each date and time unit is displayed as an individually focusable and editable segment, which allows users an easy way to edit dates using the keyboard, in any date format and locale. * **Touch friendly** – Date segments are editable using an easy to use numeric keypad, and all interactions are accessible using touch-based screen readers. * **Validation** – Integrates with HTML forms, supporting required, minimum and maximum values, custom validation functions, realtime validation, and server-side validation errors. * **Customizable** – As with all of React Aria, the DOM structure and styling of all elements can be fully customized. Read our blog post for more details about the internationalization, accessibility, and user experience features implemented by `DateField`. ## Anatomy# * * * A date field consists of a label, and a group of segments representing each unit of a date and time (e.g. years, months, days, etc.). Each segment is individually focusable and editable by the user, by typing or using the arrow keys to increment and decrement the value. This approach allows values to be formatted and parsed correctly regardless of the locale or date format, and offers an easy and error-free way to edit dates using the keyboard. `DateField` also supports optional description and error message elements, which can be used to provide more context about the field, and any validation messages. These are linked with the input via the `aria-describedby` attribute. import {DateField, DateInput, DateSegment, FieldError, Label, Text} from 'react-aria-components'; <DateField> <Label /> <DateInput> {(segment) => <DateSegment segment={segment} />} </DateInput> <Text slot="description" /> <FieldError /> </DateField> import { DateField, DateInput, DateSegment, FieldError, Label, Text } from 'react-aria-components'; <DateField> <Label /> <DateInput> {(segment) => <DateSegment segment={segment} />} </DateInput> <Text slot="description" /> <FieldError /> </DateField> import { DateField, DateInput, DateSegment, FieldError, Label, Text } from 'react-aria-components'; <DateField> <Label /> <DateInput> {(segment) => ( <DateSegment segment={segment} /> )} </DateInput> <Text slot="description" /> <FieldError /> </DateField> If the date field does not have a visible label, an `aria-label` or `aria-labelledby` prop must be passed instead to identify it to assistive technology. Note that most of this anatomy is shared with TimeField, so you can reuse many components between them if you have both. ### Internationalization# To ensure the proper date and time format in RTL locales, `DateInput` must have `display` set to either `inline`, `inline-block`, or `block`. ### Concepts# `DateField` makes use of the following concepts: @internationalized/date Represent and manipulate dates and times in a locale-aware manner. Forms Validating and submitting form data, and integrating with form libraries. ### Composed components# A `DateField` uses the following components, which may also be used standalone or reused in other components. Label A label provides context for an input element. ## Starter kits# * * * To help kick-start your project, we offer starter kits that include example implementations of all React Aria components with various styling solutions. All components are fully styled, including support for dark mode, high contrast mode, and all UI states. Each starter comes with a pre-configured Storybook that you can experiment with, or use as a starting point for your own component library. Vanilla CSS Download ZIP Preview Tailwind CSS Download ZIP Preview ## Reusable wrappers# * * * If you will use a DateField in multiple places in your app, you can wrap all of the pieces into a reusable component. This way, the DOM structure, styling code, and other logic are defined in a single place and reused everywhere to ensure consistency. This example wraps `DateField` and all of its children together into a single component which accepts a `label` prop, which is passed to the right place. It also shows how to use the `description` slot to render help text, and `FieldError` component to render validation errors. import type {DateFieldProps, DateValue, ValidationResult} from 'react-aria-components'; import {FieldError, Text} from 'react-aria-components'; interface MyDateFieldProps<T extends DateValue> extends DateFieldProps<T> { label?: string; description?: string; errorMessage?: string | ((validation: ValidationResult) => string); } function MyDateField<T extends DateValue>( { label, description, errorMessage, ...props }: MyDateFieldProps<T> ) { return ( <DateField {...props}> <Label>{label}</Label> <DateInput> {(segment) => <DateSegment segment={segment} />} </DateInput> {description && <Text slot="description">{description}</Text>} <FieldError>{errorMessage}</FieldError> </DateField> ); } <MyDateField label="Event date" /> import type { DateFieldProps, DateValue, ValidationResult } from 'react-aria-components'; import {FieldError, Text} from 'react-aria-components'; interface MyDateFieldProps<T extends DateValue> extends DateFieldProps<T> { label?: string; description?: string; errorMessage?: | string | ((validation: ValidationResult) => string); } function MyDateField<T extends DateValue>( { label, description, errorMessage, ...props }: MyDateFieldProps<T> ) { return ( <DateField {...props}> <Label>{label}</Label> <DateInput> {(segment) => <DateSegment segment={segment} />} </DateInput> {description && ( <Text slot="description">{description}</Text> )} <FieldError>{errorMessage}</FieldError> </DateField> ); } <MyDateField label="Event date" /> import type { DateFieldProps, DateValue, ValidationResult } from 'react-aria-components'; import { FieldError, Text } from 'react-aria-components'; interface MyDateFieldProps< T extends DateValue > extends DateFieldProps<T> { label?: string; description?: string; errorMessage?: | string | (( validation: ValidationResult ) => string); } function MyDateField< T extends DateValue >({ label, description, errorMessage, ...props }: MyDateFieldProps<T>) { return ( <DateField {...props} > <Label> {label} </Label> <DateInput> {(segment) => ( <DateSegment segment={segment} /> )} </DateInput> {description && ( <Text slot="description"> {description} </Text> )} <FieldError> {errorMessage} </FieldError> </DateField> ); } <MyDateField label="Event date" /> Event date mm/dd/yyyy ## Value# * * * A `DateField` displays a placeholder by default. An initial, uncontrolled value can be provided to the `DateField` using the `defaultValue` prop. Alternatively, a controlled value can be provided using the `value` prop. Date values are provided using objects in the @internationalized/date package. This library handles correct international date manipulation across calendars, time zones, and other localization concerns. `DateField` supports values of the following types: * ` CalendarDate `– a date without any time components. May be parsed from a string representation using the` parseDate `function. Use this type to represent dates where the time is not important, such as a birthday or an all day calendar event. * ` CalendarDateTime `– a date with a time, but not in any specific time zone. May be parsed from a string representation using the` parseDateTime `function. Use this type to represent times that occur at the same local time regardless of the time zone, such as the time of New Years Eve fireworks which always occur at midnight. Most times are better stored as a `ZonedDateTime`. * ` ZonedDateTime `– a date with a time in a specific time zone. May be parsed from a string representation using the` parseZonedDateTime `,` parseAbsolute `, or` parseAbsoluteToLocal `functions. Use this type to represent an exact moment in time at a particular location on Earth. import {parseDate} from '@internationalized/date'; function Example() { let [value, setValue] = React.useState(parseDate('2020-02-03')); return ( <> <MyDateField label="Date (uncontrolled)" defaultValue={parseDate('2020-02-03')} /> <MyDateField label="Date (controlled)" value={value} onChange={setValue} /> </> ); } import {parseDate} from '@internationalized/date'; function Example() { let [value, setValue] = React.useState( parseDate('2020-02-03') ); return ( <> <MyDateField label="Date (uncontrolled)" defaultValue={parseDate('2020-02-03')} /> <MyDateField label="Date (controlled)" value={value} onChange={setValue} /> </> ); } import {parseDate} from '@internationalized/date'; function Example() { let [value, setValue] = React.useState( parseDate( '2020-02-03' ) ); return ( <> <MyDateField label="Date (uncontrolled)" defaultValue={parseDate( '2020-02-03' )} /> <MyDateField label="Date (controlled)" value={value} onChange={setValue} /> </> ); } Date (uncontrolled) 2/3/2020 Date (controlled) 2/3/2020 ### Time zones# `DateField` is time zone aware when a `ZonedDateTime` object is provided as the value. In this case, the time zone abbreviation is displayed, and time zone concerns such as daylight saving time are taken into account when the value is manipulated. In most cases, your data will come from and be sent to a server as an ISO 8601 formatted string. @internationalized/date includes functions for parsing strings in multiple formats into `ZonedDateTime` objects. Which format you use will depend on what information you need to store. * ` parseZonedDateTime `– This function parses a date with an explicit time zone and optional UTC offset attached (e.g. `"2021-11-07T00:45[America/Los_Angeles]"` or `"2021-11-07T00:45-07:00[America/Los_Angeles]"`). This format preserves the maximum amount of information. If the exact local time and time zone that a user selected is important, use this format. Storing the time zone and offset that was selected rather than converting to UTC ensures that the local time is correct regardless of daylight saving rule changes (e.g. if a locale abolishes DST). Examples where this applies include calendar events, reminders, and other times that occur in a particular location. * ` parseAbsolute `– This function parses an absolute date and time that occurs at the same instant at all locations on Earth. It can be represented in UTC (e.g. `"2021-11-07T07:45:00Z"`), or stored with a particular offset (e.g. `"2021-11-07T07:45:00-07:00"`). A time zone identifier, e.g. `America/Los_Angeles`, must be passed, and the result will be converted into that time zone. Absolute times are the best way to represent events that occurred in the past, or future events where an exact time is needed, regardless of time zone. * ` parseAbsoluteToLocal `– This function parses an absolute date and time into the current user's local time zone. It is a shortcut for `parseAbsolute`, and accepts the same formats. import {parseZonedDateTime} from '@internationalized/date'; <MyDateField label="Event date" defaultValue={parseZonedDateTime('2022-11-07T00:45[America/Los_Angeles]')} /> import {parseZonedDateTime} from '@internationalized/date'; <MyDateField label="Event date" defaultValue={parseZonedDateTime( '2022-11-07T00:45[America/Los_Angeles]' )} /> import {parseZonedDateTime} from '@internationalized/date'; <MyDateField label="Event date" defaultValue={parseZonedDateTime( '2022-11-07T00:45[America/Los_Angeles]' )} /> Event date 11/7/2022, 12:45 AM PST `DateField` displays times in the time zone included in the `ZonedDateTime` object. The above example is always displayed in Pacific Standard Time because the `America/Los_Angeles` time zone identifier is provided. @internationalized/date includes functions for converting dates between time zones, or parsing a date directly into a specific time zone or the user's local time zone, as shown below. import {parseAbsoluteToLocal} from '@internationalized/date'; <MyDateField label="Event date" defaultValue={parseAbsoluteToLocal('2021-11-07T07:45:00Z')} /> import {parseAbsoluteToLocal} from '@internationalized/date'; <MyDateField label="Event date" defaultValue={parseAbsoluteToLocal( '2021-11-07T07:45:00Z' )} /> import {parseAbsoluteToLocal} from '@internationalized/date'; <MyDateField label="Event date" defaultValue={parseAbsoluteToLocal( '2021-11-07T07:45:00Z' )} /> Event date 11/7/2021, 7:45 AM UTC ### Granularity# The `granularity` prop allows you to control the smallest unit that is displayed by `DateField`. By default, `CalendarDate` values are displayed with `"day"` granularity (year, month, and day), and `CalendarDateTime` and `ZonedDateTime` values are displayed with `"minute"` granularity. More granular time values can be displayed by setting the `granularity` prop to `"second"`. In addition, when a value with a time is provided but you wish to only display the date, you can set the granularity to `"day"`. This has no effect on the actual value (it still has a time component), only on what fields are displayed. In the following example, two DateFields are synchronized with the same value, but display different granularities. function Example() { let [date, setDate] = React.useState( parseAbsoluteToLocal('2021-04-07T18:45:22Z') ); return ( <> <MyDateField label="Date and time" granularity="second" value={date} onChange={setDate} /> <MyDateField label="Date" granularity="day" value={date} onChange={setDate} /> </> ); } function Example() { let [date, setDate] = React.useState( parseAbsoluteToLocal('2021-04-07T18:45:22Z') ); return ( <> <MyDateField label="Date and time" granularity="second" value={date} onChange={setDate} /> <MyDateField label="Date" granularity="day" value={date} onChange={setDate} /> </> ); } function Example() { let [date, setDate] = React.useState( parseAbsoluteToLocal( '2021-04-07T18:45:22Z' ) ); return ( <> <MyDateField label="Date and time" granularity="second" value={date} onChange={setDate} /> <MyDateField label="Date" granularity="day" value={date} onChange={setDate} /> </> ); } Date and time 4/7/2021, 6:45:22 PM UTC Date 4/7/2021 If no `value` or `defaultValue` prop is passed, then the `granularity` prop also affects which type of value is emitted from the `onChange` event. Note that by default, time values will not have a time zone because none was supplied. You can override this by setting the `placeholderValue` prop explicitly. Values emitted from `onChange` will use the time zone of the placeholder value. import {now} from '@internationalized/date'; <MyDateField label="Event date" granularity="second" /> <MyDateField label="Event date" placeholderValue={now('America/New_York')} granularity="second" /> import {now} from '@internationalized/date'; <MyDateField label="Event date" granularity="second" /> <MyDateField label="Event date" placeholderValue={now('America/New_York')} granularity="second" /> import {now} from '@internationalized/date'; <MyDateField label="Event date" granularity="second" /> <MyDateField label="Event date" placeholderValue={now( 'America/New_York' )} granularity="second" /> Event date mm/dd/yyyy, ––:––:–– AM Event date mm/dd/yyyy, ––:––:–– AM EDT ### International calendars# `DateField` supports selecting dates in many calendar systems used around the world, including Gregorian, Hebrew, Indian, Islamic, Buddhist, and more. Dates are automatically displayed in the appropriate calendar system for the user's locale. The calendar system can be overridden using the Unicode calendar locale extension, passed to the I18nProvider component. Selected dates passed to `onChange` always use the same calendar system as the `value` or `defaultValue` prop. If no `value` or `defaultValue` is provided, then dates passed to `onChange` are always in the Gregorian calendar since this is the most commonly used. This means that even though the user selects dates in their local calendar system, applications are able to deal with dates from all users consistently. The below example displays a `DateField` in the Hindi language, using the Indian calendar. Dates emitted from `onChange` are in the Gregorian calendar. import {I18nProvider} from 'react-aria'; function Example() { let [date, setDate] = React.useState<DateValue | null>(null); return ( <I18nProvider locale="hi-IN-u-ca-indian"> <MyDateField label="Date" value={date} onChange={setDate} /> <p>Selected date: {date?.toString()}</p> </I18nProvider> ); } import {I18nProvider} from 'react-aria'; function Example() { let [date, setDate] = React.useState<DateValue | null>( null ); return ( <I18nProvider locale="hi-IN-u-ca-indian"> <MyDateField label="Date" value={date} onChange={setDate} /> <p>Selected date: {date?.toString()}</p> </I18nProvider> ); } import {I18nProvider} from 'react-aria'; function Example() { let [date, setDate] = React.useState< DateValue | null >(null); return ( <I18nProvider locale="hi-IN-u-ca-indian"> <MyDateField label="Date" value={date} onChange={setDate} /> <p> Selected date: {' '} {date ?.toString()} </p> </I18nProvider> ); } Date dd/mm/yyyy शक Selected date: ### HTML forms# DateField supports the `name` prop for integration with HTML forms. The value will be submitted to the server as an ISO 8601 formatted string according to the granularity of the value. For example, if the date field allows selecting only a date then a string such as `"2023-02-03"` will be submitted, and if it allows selecting a time then a string such as `"2023-02-03T08:45:00"` will be submitted. See the Value section above for more details about the supported value types. <MyDateField label="Birth date" name="birthday" /> <MyDateField label="Birth date" name="birthday" /> <MyDateField label="Birth date" name="birthday" /> Birth date mm/dd/yyyy ## Events# * * * `DateField` accepts an `onChange` prop which is triggered whenever the date is edited by the user. The example below uses `onChange` to update a separate element with a formatted version of the date in the user's locale and local time zone. This is done by converting the date to a native JavaScript `Date` object to pass to the formatter. import {useDateFormatter} from 'react-aria'; import {getLocalTimeZone} from '@internationalized/date'; function Example() { let [date, setDate] = React.useState(parseDate('1985-07-03')); let formatter = useDateFormatter({ dateStyle: 'full' }); return ( <> <MyDateField label="Birth date" value={date} onChange={setDate} /> <p> Selected date:{' '} {date ? formatter.format(date.toDate(getLocalTimeZone())) : '--'} </p> </> ); } import {useDateFormatter} from 'react-aria'; import {getLocalTimeZone} from '@internationalized/date'; function Example() { let [date, setDate] = React.useState( parseDate('1985-07-03') ); let formatter = useDateFormatter({ dateStyle: 'full' }); return ( <> <MyDateField label="Birth date" value={date} onChange={setDate} /> <p> Selected date: {date ? formatter.format( date.toDate(getLocalTimeZone()) ) : '--'} </p> </> ); } import {useDateFormatter} from 'react-aria'; import {getLocalTimeZone} from '@internationalized/date'; function Example() { let [date, setDate] = React.useState( parseDate( '1985-07-03' ) ); let formatter = useDateFormatter({ dateStyle: 'full' }); return ( <> <MyDateField label="Birth date" value={date} onChange={setDate} /> <p> Selected date: {' '} {date ? formatter .format( date .toDate( getLocalTimeZone() ) ) : '--'} </p> </> ); } Birth date 7/3/1985 Selected date: Wednesday, July 3, 1985 ## Validation# * * * DateField supports the `isRequired` prop to ensure the user enters a value, as well as minimum and maximum values, and custom client and server-side validation. It can also be integrated with other form libraries. See the Forms guide to learn more. To display validation errors, add a `<FieldError>` element as a child of the DateField. This allows you to render error messages from all of the above sources with consistent custom styles. import {Form, FieldError, Button} from 'react-aria-components'; <Form> <DateField name="date" isRequired> <Label>Appointment date</Label> <DateInput> {segment => <DateSegment segment={segment} />} </DateInput> <FieldError /> </DateField> <Button type="submit">Submit</Button> </Form> import { Button, FieldError, Form } from 'react-aria-components'; <Form> <DateField name="date" isRequired> <Label>Appointment date</Label> <DateInput> {(segment) => <DateSegment segment={segment} />} </DateInput> <FieldError /> </DateField> <Button type="submit">Submit</Button> </Form> import { Button, FieldError, Form } from 'react-aria-components'; <Form> <DateField name="date" isRequired > <Label> Appointment date </Label> <DateInput> {(segment) => ( <DateSegment segment={segment} /> )} </DateInput> <FieldError /> </DateField> <Button type="submit"> Submit </Button> </Form> Appointment date mm/dd/yyyy Submit Show CSS .react-aria-DateSegment { &[data-invalid] { color: var(--invalid-color); &:focus { background: var(--highlight-background-invalid); color: var(--highlight-foreground); } } } .react-aria-DateField { .react-aria-FieldError { font-size: 12px; color: var(--invalid-color); } } .react-aria-DateSegment { &[data-invalid] { color: var(--invalid-color); &:focus { background: var(--highlight-background-invalid); color: var(--highlight-foreground); } } } .react-aria-DateField { .react-aria-FieldError { font-size: 12px; color: var(--invalid-color); } } .react-aria-DateSegment { &[data-invalid] { color: var(--invalid-color); &:focus { background: var(--highlight-background-invalid); color: var(--highlight-foreground); } } } .react-aria-DateField { .react-aria-FieldError { font-size: 12px; color: var(--invalid-color); } } By default, `FieldError` displays default validation messages provided by the browser. See Customizing error messages in the Forms guide to learn how to provide your own custom errors. ### Minimum and maximum values# The `minValue` and `maxValue` props can also be used to ensure the value is within a specific range. This example only accepts dates after today. import {today} from '@internationalized/date'; <Form> <MyDateField label="Appointment date" minValue={today(getLocalTimeZone())} defaultValue={parseDate('2022-02-03')} /> <Button type="submit">Submit</Button> </Form> import {today} from '@internationalized/date'; <Form> <MyDateField label="Appointment date" minValue={today(getLocalTimeZone())} defaultValue={parseDate('2022-02-03')} /> <Button type="submit">Submit</Button> </Form> import {today} from '@internationalized/date'; <Form> <MyDateField label="Appointment date" minValue={today( getLocalTimeZone() )} defaultValue={parseDate( '2022-02-03' )} /> <Button type="submit"> Submit </Button> </Form> Appointment date 2/3/2022 Submit ### Custom validation# The `validate` function can be used to perform custom validation logic. It receives the current field value, and should return a string or array of strings representing one or more error messages if the value is invalid. This example validates that the selected date is a weekday and not a weekend according to the current locale. import {isWeekend} from '@internationalized/date'; import {useLocale} from 'react-aria'; function Example() { let { locale } = useLocale(); return ( <Form> <MyDateField label="Appointment date" validate={(date) => date && isWeekend(date, locale) ? 'We are closed on weekends.' : null} defaultValue={parseDate('2023-10-28')} /> <Button type="submit">Submit</Button> </Form> ); } import {isWeekend} from '@internationalized/date'; import {useLocale} from 'react-aria'; function Example() { let { locale } = useLocale(); return ( <Form> <MyDateField label="Appointment date" validate={(date) => date && isWeekend(date, locale) ? 'We are closed on weekends.' : null} defaultValue={parseDate('2023-10-28')} /> <Button type="submit">Submit</Button> </Form> ); } import {isWeekend} from '@internationalized/date'; import {useLocale} from 'react-aria'; function Example() { let { locale } = useLocale(); return ( <Form> <MyDateField label="Appointment date" validate={(date) => date && isWeekend( date, locale ) ? 'We are closed on weekends.' : null} defaultValue={parseDate( '2023-10-28' )} /> <Button type="submit"> Submit </Button> </Form> ); } Appointment date 10/28/2023 Submit ### Description# The `description` slot can be used to associate additional help text with a date field. <DateField granularity="hour"> <Label>Appointment time</Label> <DateInput> {segment => <DateSegment segment={segment} />} </DateInput> <Text slot="description">Please select a weekday between 9 AM and 5 PM.</Text></DateField> <DateField granularity="hour"> <Label>Appointment time</Label> <DateInput> {(segment) => <DateSegment segment={segment} />} </DateInput> <Text slot="description"> Please select a weekday between 9 AM and 5 PM. </Text></DateField> <DateField granularity="hour"> <Label> Appointment time </Label> <DateInput> {(segment) => ( <DateSegment segment={segment} /> )} </DateInput> <Text slot="description"> Please select a weekday between 9 AM and 5 PM. </Text></DateField> Appointment time mm/dd/yyyy, –– AM Please select a weekday between 9 AM and 5 PM. Show CSS .react-aria-DateField { [slot=description] { font-size: 12px; } } .react-aria-DateField { [slot=description] { font-size: 12px; } } .react-aria-DateField { [slot=description] { font-size: 12px; } } ## Format options# * * * ### Placeholder value# When no value is set, a placeholder is shown. The format of the placeholder is influenced by the `granularity` and `placeholderValue` props. `placeholderValue` also controls the default values of each segment when the user first interacts with them, e.g. using the up and down arrow keys. By default, the `placeholderValue` is the current date at midnight, but you can set it to a more appropriate value if needed. import {CalendarDate} from '@internationalized/date'; <MyDateField label="Birth date" placeholderValue={new CalendarDate(1980, 1, 1)} /> import {CalendarDate} from '@internationalized/date'; <MyDateField label="Birth date" placeholderValue={new CalendarDate(1980, 1, 1)} /> import {CalendarDate} from '@internationalized/date'; <MyDateField label="Birth date" placeholderValue={new CalendarDate( 1980, 1, 1 )} /> Birth date mm/dd/yyyy ### Hide time zone# When a `ZonedDateTime` object is provided as the value to `DateField`, the time zone abbreviation is displayed by default. However, if this is displayed elsewhere or implicit based on the usecase, it can be hidden using the `hideTimeZone` option. <MyDateField label="Appointment time" defaultValue={parseZonedDateTime('2022-11-07T10:45[America/Los_Angeles]')} hideTimeZone /> <MyDateField label="Appointment time" defaultValue={parseZonedDateTime( '2022-11-07T10:45[America/Los_Angeles]' )} hideTimeZone /> <MyDateField label="Appointment time" defaultValue={parseZonedDateTime( '2022-11-07T10:45[America/Los_Angeles]' )} hideTimeZone /> Appointment time 11/7/2022, 10:45 AM ### Hour cycle# By default, `DateField` displays times in either 12 or 24 hour hour format depending on the user's locale. However, this can be overridden using the `hourCycle` prop if needed for a specific usecase. This example forces `DateField` to use 24-hour time, regardless of the locale. <MyDateField label="Appointment time" granularity="minute" hourCycle={24} /> <MyDateField label="Appointment time" granularity="minute" hourCycle={24} /> <MyDateField label="Appointment time" granularity="minute" hourCycle={24} /> Appointment time mm/dd/yyyy, ––:–– ## Props# * * * ### DateField# | Name | Type | Default | Description | | --- | --- | --- | --- | | `minValue` | ` DateValue | null` | — | The minimum allowed date that a user may select. | | `maxValue` | ` DateValue | null` | — | The maximum allowed date that a user may select. | | `isDateUnavailable` | `( (date: DateValue )) => boolean` | — | Callback that is called for each date of the calendar. If it returns true, then the date is unavailable. | | `placeholderValue` | ` DateValue | null` | — | A placeholder date that influences the format of the placeholder shown when no value is selected. Defaults to today's date at midnight. | | `hourCycle` | `12 | 24` | — | Whether to display the time in 12 or 24 hour format. By default, this is determined by the user's locale. | | `granularity` | ` Granularity ` | — | Determines the smallest unit that is displayed in the date picker. By default, this is `"day"` for dates, and `"minute"` for times. | | `hideTimeZone` | `boolean` | `false` | Whether to hide the time zone abbreviation. | | `shouldForceLeadingZeros` | `boolean` | — | Whether to always show leading zeros in the month, day, and hour fields. By default, this is determined by the user's locale. | | `isDisabled` | `boolean` | — | Whether the input is disabled. | | `isReadOnly` | `boolean` | — | Whether the input can be selected but not changed by the user. | | `isRequired` | `boolean` | — | Whether user input is required on the input before form submission. | | `isInvalid` | `boolean` | — | Whether the input value is invalid. | | `validate` | `( (value: MappedDateValue < DateValue > )) => ValidationError | true | null | undefined` | — | A function that returns an error message if a given value is invalid. Validation errors are displayed to the user when the form is submitted if `validationBehavior="native"`. For realtime validation, use the `isInvalid` prop instead. | | `autoFocus` | `boolean` | — | Whether the element should receive focus on render. | | `value` | ` DateValue | null` | — | The current value (controlled). | | `defaultValue` | ` DateValue | null` | — | The default value (uncontrolled). | | `name` | `string` | — | The name of the input element, used when submitting an HTML form. See MDN. | | `validationBehavior` | `'native' | 'aria'` | `'native'` | Whether to use native HTML form validation to prevent form submission when the value is missing or invalid, or mark the field as required or invalid via ARIA. | | `children` | `ReactNode | ( (values: DateFieldRenderProps & & { defaultChildren: ReactNode | | undefined } )) => ReactNode` | — | The children of the component. A function may be provided to alter the children based on component state. | | `className` | `string | ( (values: DateFieldRenderProps & & { defaultClassName: string | | undefined } )) => string` | — | The CSS className for the element. A function may be provided to compute the class based on component state. | | `style` | `CSSProperties | ( (values: DateFieldRenderProps & & { defaultStyle: CSSProperties } )) => CSSProperties | undefined` | — | The inline style for the element. A function may be provided to compute the style based on component state. | Events | Name | Type | Description | | --- | --- | --- | | `onFocus` | `( (e: FocusEvent<Target> )) => void` | Handler that is called when the element receives focus. | | `onBlur` | `( (e: FocusEvent<Target> )) => void` | Handler that is called when the element loses focus. | | `onFocusChange` | `( (isFocused: boolean )) => void` | Handler that is called when the element's focus status changes. | | `onKeyDown` | `( (e: KeyboardEvent )) => void` | Handler that is called when a key is pressed. | | `onKeyUp` | `( (e: KeyboardEvent )) => void` | Handler that is called when a key is released. | | `onChange` | `( (value: MappedDateValue < DateValue > | | null )) => void` | Handler that is called when the value changes. | Layout | Name | Type | Description | | --- | --- | --- | | `slot` | `string | null` | A slot name for the component. Slots allow the component to receive props from a parent component. An explicit `null` value indicates that the local props completely override all props received from a parent. | Accessibility | Name | Type | Description | | --- | --- | --- | | `id` | `string` | The element's unique identifier. See MDN. | | `aria-label` | `string` | Defines a string value that labels the current element. | | `aria-labelledby` | `string` | Identifies the element (or elements) that labels the current element. | | `aria-describedby` | `string` | Identifies the element (or elements) that describes the object. | | `aria-details` | `string` | Identifies the element (or elements) that provide a detailed, extended description for the object. | ### Label# A `<Label>` accepts all HTML attributes. ### DateInput# The `<DateInput>` component renders a group of date segments. It accepts a function as its `children`, which is called to render a `<DateSegment>` for each segment. Show props | Name | Type | Description | | --- | --- | --- | | `children` | `( (segment: DateSegment )) => ReactElement` | | | `className` | `string | ( (values: DateInputRenderProps & & { defaultClassName: string | | undefined } )) => string` | The CSS className for the element. A function may be provided to compute the class based on component state. | | `style` | `CSSProperties | ( (values: DateInputRenderProps & & { defaultStyle: CSSProperties } )) => CSSProperties | undefined` | The inline style for the element. A function may be provided to compute the style based on component state. | Layout | Name | Type | Description | | --- | --- | --- | | `slot` | `string | null` | A slot name for the component. Slots allow the component to receive props from a parent component. An explicit `null` value indicates that the local props completely override all props received from a parent. | ### DateSegment# The `<DateSegment>` component renders an individual segment. Show props | Name | Type | Description | | --- | --- | --- | | `segment` | ` DateSegment ` | | | `children` | `ReactNode | ( (values: DateSegmentRenderProps & & { defaultChildren: ReactNode | | undefined } )) => ReactNode` | The children of the component. A function may be provided to alter the children based on component state. | | `className` | `string | ( (values: DateSegmentRenderProps & & { defaultClassName: string | | undefined } )) => string` | The CSS className for the element. A function may be provided to compute the class based on component state. | | `style` | `CSSProperties | ( (values: DateSegmentRenderProps & & { defaultStyle: CSSProperties } )) => CSSProperties | undefined` | The inline style for the element. A function may be provided to compute the style based on component state. | Events | Name | Type | Description | | --- | --- | --- | | `onHoverStart` | `( (e: HoverEvent )) => void` | Handler that is called when a hover interaction starts. | | `onHoverEnd` | `( (e: HoverEvent )) => void` | Handler that is called when a hover interaction ends. | | `onHoverChange` | `( (isHovering: boolean )) => void` | Handler that is called when the hover state changes. | ### FieldError# A `<FieldError>` displays validation errors. Show props | Name | Type | Description | | --- | --- | --- | | `children` | `ReactNode | ( (values: FieldErrorRenderProps & & { defaultChildren: ReactNode | | undefined } )) => ReactNode` | The children of the component. A function may be provided to alter the children based on component state. | | `className` | `string | ( (values: FieldErrorRenderProps & & { defaultClassName: string | | undefined } )) => string` | The CSS className for the element. A function may be provided to compute the class based on component state. | | `style` | `CSSProperties | ( (values: FieldErrorRenderProps & & { defaultStyle: CSSProperties } )) => CSSProperties | undefined` | The inline style for the element. A function may be provided to compute the style based on component state. | Accessibility | Name | Type | Description | | --- | --- | --- | | `id` | `string` | The element's unique identifier. See MDN. | ## Styling# * * * React Aria components can be styled in many ways, including using CSS classes, inline styles, utility classes (e.g. Tailwind), CSS-in-JS (e.g. Styled Components), etc. By default, all components include a builtin `className` attribute which can be targeted using CSS selectors. These follow the `react-aria-ComponentName` naming convention. .react-aria-DateField { /* ... */ } .react-aria-DateField { /* ... */ } .react-aria-DateField { /* ... */ } A custom `className` can also be specified on any component. This overrides the default `className` provided by React Aria with your own. <DateInput className="my-date-input"> {/* ... */} </DateInput> <DateInput className="my-date-input"> {/* ... */} </DateInput> <DateInput className="my-date-input"> {/* ... */} </DateInput> In addition, some components support multiple UI states (e.g. focused, placeholder, readonly, etc.). React Aria components expose states using data attributes, which you can target in CSS selectors. For example: .react-aria-DateSegment[data-placeholder] { /* ... */ } .react-aria-DateSegment[data-readonly] { /* ... */ } .react-aria-DateSegment[data-placeholder] { /* ... */ } .react-aria-DateSegment[data-readonly] { /* ... */ } .react-aria-DateSegment[data-placeholder] { /* ... */ } .react-aria-DateSegment[data-readonly] { /* ... */ } The `className` and `style` props also accept functions which receive states for styling. This lets you dynamically determine the classes or styles to apply, which is useful when using utility CSS libraries like Tailwind. <DateSegment className={({ isPlaceholder }) => isPlaceholder ? 'bg-gray-300' : 'bg-gray-600'} /> <DateSegment className={({ isPlaceholder }) => isPlaceholder ? 'bg-gray-300' : 'bg-gray-600'} /> <DateSegment className={( { isPlaceholder } ) => isPlaceholder ? 'bg-gray-300' : 'bg-gray-600'} /> Render props may also be used as children to alter what elements are rendered based on the current state. For example, you could render the placeholder as a separate element to always reserve space for it. <DateSegment> {({ text, placeholder, isPlaceholder }) => ( <> <span style={{ visibility: isPlaceholder ? 'visible' : 'hidden' }}> {placeholder} </span> {isPlaceholder ? '' : text} </> )} </DateSegment> <DateSegment> {({ text, placeholder, isPlaceholder }) => ( <> <span style={{ visibility: isPlaceholder ? 'visible' : 'hidden' }} > {placeholder} </span> {isPlaceholder ? '' : text} </> )} </DateSegment> <DateSegment> {( { text, placeholder, isPlaceholder } ) => ( <> <span style={{ visibility: isPlaceholder ? 'visible' : 'hidden' }} > {placeholder} </span> {isPlaceholder ? '' : text} </> )} </DateSegment> The states, selectors, and render props for each component used in a `DateField` are documented below. ### DateField# A `DateField` can be targeted with the `.react-aria-DateField` CSS selector, or by overriding with a custom `className`. It supports the following states and render props: | Name | CSS Selector | Description | | --- | --- | --- | | `state` | `—` | State of the date field. | | `isInvalid` | `[data-invalid]` | Whether the date field is invalid. | | `isDisabled` | `[data-disabled]` | Whether the date field is disabled. | ### Label# A `Label` can be targeted with the `.react-aria-Label` CSS selector, or by overriding with a custom `className`. ### DateInput# A `DateInput` can be targeted with the `.react-aria-DateInput` CSS selector, or by overriding with a custom `className`. It supports the following states: | Name | CSS Selector | Description | | --- | --- | --- | | `isHovered` | `[data-hovered]` | Whether the date input is currently hovered with a mouse. | | `isFocusWithin` | `[data-focus-within]` | Whether an element within the date input is focused, either via a mouse or keyboard. | | `isFocusVisible` | `[data-focus-visible]` | Whether an element within the date input is keyboard focused. | | `isDisabled` | `[data-disabled]` | Whether the date input is disabled. | | `isInvalid` | `[data-invalid]` | Whether the date input is invalid. | ### DateSegment# A `DateSegment` can be targeted with the `.react-aria-DateSegment` CSS selector, or by overriding with a custom `className`. It supports the following states and render props: | Name | CSS Selector | Description | | --- | --- | --- | | `isHovered` | `[data-hovered]` | Whether the segment is currently hovered with a mouse. | | `isFocused` | `[data-focused]` | Whether the segment is focused, either via a mouse or keyboard. | | `isFocusVisible` | `[data-focus-visible]` | Whether the segment is keyboard focused. | | `isPlaceholder` | `[data-placeholder]` | Whether the value is a placeholder. | | `isReadOnly` | `[data-readonly]` | Whether the segment is read only. | | `isDisabled` | `[data-disabled]` | Whether the date field is disabled. | | `isInvalid` | `[data-invalid]` | Whether the date field is in an invalid state. | | `type` | `[data-type="..."]` | The type of segment. Values include `literal`, `year`, `month`, `day`, etc. | | `text` | `—` | The formatted text for the segment. | | `placeholder` | `—` | A placeholder string for the segment. | ### Text# The help text elements within a `DateField` can be targeted with the `[slot=description]` and `[slot=errorMessage]` CSS selectors, or by adding a custom `className`. ### FieldError# A `FieldError` can be targeted with the `.react-aria-FieldError` CSS selector, or by overriding with a custom `className`. It supports the following render props: | Name | Description | | --- | --- | | `isInvalid` | Whether the input value is invalid. | | `validationErrors` | The current error messages for the input if it is invalid, otherwise an empty array. | | `validationDetails` | The native validation details for the input. | ## Advanced customization# * * * ### Composition# If you need to customize one of the components within a `DateField`, such as `Label` or `DateSegment`, in many cases you can create a wrapper component. This lets you customize the props passed to the component. function MyDateSegment(props) { return <MyDateSegment {...props} className="my-date-segment" /> } function MyDateSegment(props) { return ( <MyDateSegment {...props} className="my-date-segment" /> ); } function MyDateSegment( props ) { return ( <MyDateSegment {...props} className="my-date-segment" /> ); } ### Contexts# All React Aria Components export a corresponding context that can be used to send props to them from a parent element. This enables you to build your own compositional APIs similar to those found in React Aria Components itself. You can send any prop or ref via context that you could pass to the corresponding component. The local props and ref on the component are merged with the ones passed via context, with the local props taking precedence (following the rules documented in mergeProps). The components in a DateField support the following contexts: | Component | Context | Props | Ref | | --- | --- | --- | --- | | `DateField` | `DateFieldContext` | ` DateFieldProps ` | `HTMLDivElement` | This example shows a `FieldGroup` component that renders a group of date fields with a title and optional error message. It uses the useId hook to generate a unique id for the error message. All of the child DateFields are marked invalid and associated with the error message via the `aria-describedby` attribute passed to the `DateFieldContext` provider. import {DateFieldContext} from 'react-aria-components'; import {useId} from 'react-aria'; interface FieldGroupProps { title?: string; children?: React.ReactNode; errorMessage?: string; } function FieldGroup({ title, children, errorMessage }: FieldGroupProps) { let errorId = useId(); return ( <fieldset> <legend>{title}</legend> <DateFieldContext.Provider value={{ isInvalid: !!errorMessage, 'aria-describedby': errorMessage ? errorId : undefined }} > {children} </DateFieldContext.Provider> {errorMessage && ( <small id={errorId} className="invalid">{errorMessage}</small> )} </fieldset> ); } <FieldGroup title="Dates" errorMessage="Tickets must go on sale before event." > <MyDateField label="Event date" defaultValue={parseDate('2023-07-12')} /> <MyDateField label="Ticket sale date" defaultValue={parseDate('2023-08-03')} /> </FieldGroup> import {DateFieldContext} from 'react-aria-components'; import {useId} from 'react-aria'; interface FieldGroupProps { title?: string; children?: React.ReactNode; errorMessage?: string; } function FieldGroup( { title, children, errorMessage }: FieldGroupProps ) { let errorId = useId(); return ( <fieldset> <legend>{title}</legend> <DateFieldContext.Provider value={{ isInvalid: !!errorMessage, 'aria-describedby': errorMessage ? errorId : undefined }} > {children} </DateFieldContext.Provider> {errorMessage && ( <small id={errorId} className="invalid"> {errorMessage} </small> )} </fieldset> ); } <FieldGroup title="Dates" errorMessage="Tickets must go on sale before event." > <MyDateField label="Event date" defaultValue={parseDate('2023-07-12')} /> <MyDateField label="Ticket sale date" defaultValue={parseDate('2023-08-03')} /> </FieldGroup> import {DateFieldContext} from 'react-aria-components'; import {useId} from 'react-aria'; interface FieldGroupProps { title?: string; children?: React.ReactNode; errorMessage?: string; } function FieldGroup( { title, children, errorMessage }: FieldGroupProps ) { let errorId = useId(); return ( <fieldset> <legend> {title} </legend> <DateFieldContext.Provider value={{ isInvalid: !!errorMessage, 'aria-describedby': errorMessage ? errorId : undefined }} > {children} </DateFieldContext.Provider> {errorMessage && ( <small id={errorId} className="invalid" > {errorMessage} </small> )} </fieldset> ); } <FieldGroup title="Dates" errorMessage="Tickets must go on sale before event." > <MyDateField label="Event date" defaultValue={parseDate( '2023-07-12' )} /> <MyDateField label="Ticket sale date" defaultValue={parseDate( '2023-08-03' )} /> </FieldGroup> Dates Event date 7/12/2023 Ticket sale date 8/3/2023 Tickets must go on sale before event. Show CSS fieldset { padding: 1.5em; width: fit-content; } .invalid { color: var(--invalid-color); margin-top: 1em; display: block; } fieldset { padding: 1.5em; width: fit-content; } .invalid { color: var(--invalid-color); margin-top: 1em; display: block; } fieldset { padding: 1.5em; width: fit-content; } .invalid { color: var(--invalid-color); margin-top: 1em; display: block; } ### Custom children# DateField passes props to its child components, such as the label, via their associated contexts. These contexts are exported so you can also consume them in your own custom components. This enables you to reuse existing components from your app or component library together with React Aria Components. | Component | Context | Props | Ref | | --- | --- | --- | --- | | `Label` | `LabelContext` | ` LabelProps ` | `HTMLLabelElement` | | `Text` | `TextContext` | ` TextProps ` | `HTMLElement` | This example consumes from `LabelContext` in an existing styled label component to make it compatible with React Aria Components. The `useContextProps` hook merges the local props and ref with the ones provided via context by DateField. import type {LabelProps} from 'react-aria-components'; import {LabelContext, useContextProps} from 'react-aria-components'; const MyCustomLabel = React.forwardRef( (props: LabelProps, ref: React.ForwardedRef<HTMLLabelElement>) => { // Merge the local props and ref with the ones provided via context. [props, ref] = useContextProps(props, ref, LabelContext); // ... your existing Label component return <label {...props} ref={ref} />; } ); import type {LabelProps} from 'react-aria-components'; import { LabelContext, useContextProps } from 'react-aria-components'; const MyCustomLabel = React.forwardRef( ( props: LabelProps, ref: React.ForwardedRef<HTMLLabelElement> ) => { // Merge the local props and ref with the ones provided via context. [props, ref] = useContextProps( props, ref, LabelContext ); // ... your existing Label component return <label {...props} ref={ref} />; } ); import type {LabelProps} from 'react-aria-components'; import { LabelContext, useContextProps } from 'react-aria-components'; const MyCustomLabel = React.forwardRef( ( props: LabelProps, ref: React.ForwardedRef< HTMLLabelElement > ) => { // Merge the local props and ref with the ones provided via context. [props, ref] = useContextProps( props, ref, LabelContext ); // ... your existing Label component return ( <label {...props} ref={ref} /> ); } ); Now you can use `MyCustomLabel` within a `DateField`, in place of the builtin React Aria Components `Label`. <DateField> <MyCustomLabel>Name</MyCustomLabel> <DateInput> {segment => <DateSegment segment={segment} />} </DateInput> </DateField> <DateField> <MyCustomLabel>Name</MyCustomLabel> <DateInput> {segment => <DateSegment segment={segment} />} </DateInput> </DateField> <DateField> <MyCustomLabel> Name </MyCustomLabel> <DateInput> {(segment) => ( <DateSegment segment={segment} /> )} </DateInput> </DateField> ### State# DateField provides a `DateFieldState` object to its children via `DateFieldStateContext`. This can be used to access and manipulate the date field's state. This example shows a `DateFormat` component that can be placed within a `DateField` to display the expected date format. import {DateFieldStateContext} from 'react-aria-components'; import {useLocale} from 'react-aria'; function DateFormat() { let state = React.useContext(DateFieldStateContext)!; let { locale } = useLocale(); let displayNames = new Intl.DisplayNames(locale, { type: 'dateTimeField' }); let format = state.segments.map((segment) => segment.type === 'literal' ? segment.text : displayNames.of(segment.type) ).join(' '); return <small>{format}</small>; } <DateField defaultValue={today(getLocalTimeZone())}> <Label>Date</Label> <DateInput> {(segment) => <DateSegment segment={segment} />} </DateInput> <DateFormat /></DateField> import {DateFieldStateContext} from 'react-aria-components'; import {useLocale} from 'react-aria'; function DateFormat() { let state = React.useContext(DateFieldStateContext)!; let { locale } = useLocale(); let displayNames = new Intl.DisplayNames(locale, { type: 'dateTimeField' }); let format = state.segments.map((segment) => segment.type === 'literal' ? segment.text : displayNames.of(segment.type) ).join(' '); return <small>{format}</small>; } <DateField defaultValue={today(getLocalTimeZone())}> <Label>Date</Label> <DateInput> {(segment) => <DateSegment segment={segment} />} </DateInput> <DateFormat /></DateField> import {DateFieldStateContext} from 'react-aria-components'; import {useLocale} from 'react-aria'; function DateFormat() { let state = React .useContext( DateFieldStateContext )!; let { locale } = useLocale(); let displayNames = new Intl .DisplayNames( locale, { type: 'dateTimeField' } ); let format = state .segments.map( (segment) => segment.type === 'literal' ? segment.text : displayNames .of( segment .type ) ).join(' '); return ( <small> {format} </small> ); } <DateField defaultValue={today( getLocalTimeZone() )} > <Label>Date</Label> <DateInput> {(segment) => ( <DateSegment segment={segment} /> )} </DateInput> <DateFormat /></DateField> Date 6/18/2025 month / day / year ### Hooks# If you need to customize things even further, such as accessing internal state or customizing DOM structure, you can drop down to the lower level Hook-based API. See useDateField for more details. --- ## Page: https://react-spectrum.adobe.com/react-aria/DatePicker.html # DatePicker A date picker combines a DateField and a Calendar popover to allow users to enter or select a date and time value. <table><tbody><tr><th>install</th><td><code>yarn add react-aria-components</code></td></tr><tr><th>version</th><td>1.10.1</td></tr><tr><th>usage</th><td><code><span>import</span> {DatePicker} <span>from</span> <span>'react-aria-components'</span></code></td></tr></tbody></table> View repository GitHub View package NPM ## Example# * * * import {Button, Calendar, CalendarCell, CalendarGrid, DateInput, DatePicker, DateSegment, Dialog, Group, Heading, Label, Popover} from 'react-aria-components'; <DatePicker> <Label>Date</Label> <Group> <DateInput> {(segment) => <DateSegment segment={segment} />} </DateInput> <Button>▼</Button> </Group> <Popover> <Dialog> <Calendar> <header> <Button slot="previous">◀</Button> <Heading /> <Button slot="next">▶</Button> </header> <CalendarGrid> {(date) => <CalendarCell date={date} />} </CalendarGrid> </Calendar> </Dialog> </Popover> </DatePicker> import { Button, Calendar, CalendarCell, CalendarGrid, DateInput, DatePicker, DateSegment, Dialog, Group, Heading, Label, Popover } from 'react-aria-components'; <DatePicker> <Label>Date</Label> <Group> <DateInput> {(segment) => <DateSegment segment={segment} />} </DateInput> <Button>▼</Button> </Group> <Popover> <Dialog> <Calendar> <header> <Button slot="previous">◀</Button> <Heading /> <Button slot="next">▶</Button> </header> <CalendarGrid> {(date) => <CalendarCell date={date} />} </CalendarGrid> </Calendar> </Dialog> </Popover> </DatePicker> import { Button, Calendar, CalendarCell, CalendarGrid, DateInput, DatePicker, DateSegment, Dialog, Group, Heading, Label, Popover } from 'react-aria-components'; <DatePicker> <Label>Date</Label> <Group> <DateInput> {(segment) => ( <DateSegment segment={segment} /> )} </DateInput> <Button>▼</Button> </Group> <Popover> <Dialog> <Calendar> <header> <Button slot="previous"> ◀ </Button> <Heading /> <Button slot="next"> ▶ </Button> </header> <CalendarGrid> {(date) => ( <CalendarCell date={date} /> )} </CalendarGrid> </Calendar> </Dialog> </Popover> </DatePicker> Date mm/dd/yyyy ▼ Show CSS @import "@react-aria/example-theme"; .react-aria-DatePicker { color: var(--text-color); .react-aria-Group { display: flex; width: fit-content; align-items: center; } .react-aria-Button { background: var(--highlight-background); color: var(--highlight-foreground); border: 2px solid var(--field-background); forced-color-adjust: none; border-radius: 4px; border: none; margin-left: -1.929rem; width: 1.429rem; height: 1.429rem; padding: 0; font-size: 0.857rem; box-sizing: content-box; &[data-pressed] { box-shadow: none; background: var(--highlight-background); } &[data-focus-visible] { outline: 2px solid var(--focus-ring-color); outline-offset: 2px; } } .react-aria-DateInput { padding: 4px 2.5rem 4px 8px; } } .react-aria-Popover[data-trigger=DatePicker] { max-width: unset; } @import "@react-aria/example-theme"; .react-aria-DatePicker { color: var(--text-color); .react-aria-Group { display: flex; width: fit-content; align-items: center; } .react-aria-Button { background: var(--highlight-background); color: var(--highlight-foreground); border: 2px solid var(--field-background); forced-color-adjust: none; border-radius: 4px; border: none; margin-left: -1.929rem; width: 1.429rem; height: 1.429rem; padding: 0; font-size: 0.857rem; box-sizing: content-box; &[data-pressed] { box-shadow: none; background: var(--highlight-background); } &[data-focus-visible] { outline: 2px solid var(--focus-ring-color); outline-offset: 2px; } } .react-aria-DateInput { padding: 4px 2.5rem 4px 8px; } } .react-aria-Popover[data-trigger=DatePicker] { max-width: unset; } @import "@react-aria/example-theme"; .react-aria-DatePicker { color: var(--text-color); .react-aria-Group { display: flex; width: fit-content; align-items: center; } .react-aria-Button { background: var(--highlight-background); color: var(--highlight-foreground); border: 2px solid var(--field-background); forced-color-adjust: none; border-radius: 4px; border: none; margin-left: -1.929rem; width: 1.429rem; height: 1.429rem; padding: 0; font-size: 0.857rem; box-sizing: content-box; &[data-pressed] { box-shadow: none; background: var(--highlight-background); } &[data-focus-visible] { outline: 2px solid var(--focus-ring-color); outline-offset: 2px; } } .react-aria-DateInput { padding: 4px 2.5rem 4px 8px; } } .react-aria-Popover[data-trigger=DatePicker] { max-width: unset; } ## Features# * * * A date picker can be built using `<input type="date">`, but this is very limited in functionality, lacking in internationalization capabilities, inconsistent between browsers, and difficult to style. `DatePicker` helps achieve accessible and international date and time pickers that can be styled as needed. * **Dates and times** – Support for dates and times with configurable granularity. * **International** – Support for 13 calendar systems used around the world, including Gregorian, Buddhist, Islamic, Persian, and more. Locale-specific formatting, number systems, hour cycles, and right-to-left support are available as well. * **Time zone aware** – Dates and times can optionally include a time zone. All modifications follow time zone rules such as daylight saving time. * **Accessible** – Each date and time unit is displayed as an individually focusable and editable segment, which allows users an easy way to edit dates using the keyboard, in any date format and locale. Users can also open a calendar popover to select dates in a standard month grid. Localized screen reader messages are included to announce when the selection and visible date range change. * **Touch friendly** – Date segments are editable using an easy to use numeric keypad, and all interactions are accessible using touch-based screen readers. * **Validation** – Integrates with HTML forms, supporting required, minimum and maximum values, unavailable dates, custom validation functions, realtime validation, and server-side validation errors. * **Customizable** – As with all of React Aria, the DOM structure and styling of all elements can be fully customized. Read our blog post for more details about the internationalization, accessibility, and user experience features implemented by `DatePicker`. ## Anatomy# * * * A date picker consists of a label, and group containing a date field and a button. Clicking the button opens a popup containing a calendar. The date field includes segments representing each unit of a date and time (e.g. years, months, days, etc.), each of which is individually focusable and editable using the keyboard. The calendar popup offers a more visual way of choosing a date. `DatePicker` also supports optional description and error message elements, which can be used to provide more context about the field, and any validation messages. These are linked with the input via the `aria-describedby` attribute. import {Button, Calendar, CalendarCell, CalendarGrid, CalendarGridBody, CalendarGridHeader, CalendarHeaderCell, DateInput, DatePicker, DateSegment, Dialog, FieldError, Group, Heading, Label, Popover, Text} from 'react-aria-components'; <DatePicker> <Label /> <Group> <DateInput> {(segment) => <DateSegment segment={segment} />} </DateInput> <Button /> </Group> <Text slot="description" /> <FieldError /> <Popover> <Dialog> <Calendar> <Button slot="previous" /> <Heading /> <Button slot="next" /> <CalendarGrid> <CalendarGridHeader> {(day) => <CalendarHeaderCell />} </CalendarGridHeader> <CalendarGridBody> {(date) => <CalendarCell date={date} />} </CalendarGridBody> </CalendarGrid> <Text slot="errorMessage" /> </Calendar> </Dialog> </Popover> </DatePicker> import { Button, Calendar, CalendarCell, CalendarGrid, CalendarGridBody, CalendarGridHeader, CalendarHeaderCell, DateInput, DatePicker, DateSegment, Dialog, FieldError, Group, Heading, Label, Popover, Text } from 'react-aria-components'; <DatePicker> <Label /> <Group> <DateInput> {(segment) => <DateSegment segment={segment} />} </DateInput> <Button /> </Group> <Text slot="description" /> <FieldError /> <Popover> <Dialog> <Calendar> <Button slot="previous" /> <Heading /> <Button slot="next" /> <CalendarGrid> <CalendarGridHeader> {(day) => <CalendarHeaderCell />} </CalendarGridHeader> <CalendarGridBody> {(date) => <CalendarCell date={date} />} </CalendarGridBody> </CalendarGrid> <Text slot="errorMessage" /> </Calendar> </Dialog> </Popover> </DatePicker> import { Button, Calendar, CalendarCell, CalendarGrid, CalendarGridBody, CalendarGridHeader, CalendarHeaderCell, DateInput, DatePicker, DateSegment, Dialog, FieldError, Group, Heading, Label, Popover, Text } from 'react-aria-components'; <DatePicker> <Label /> <Group> <DateInput> {(segment) => ( <DateSegment segment={segment} /> )} </DateInput> <Button /> </Group> <Text slot="description" /> <FieldError /> <Popover> <Dialog> <Calendar> <Button slot="previous" /> <Heading /> <Button slot="next" /> <CalendarGrid> <CalendarGridHeader> {(day) => ( <CalendarHeaderCell /> )} </CalendarGridHeader> <CalendarGridBody> {(date) => ( <CalendarCell date={date} /> )} </CalendarGridBody> </CalendarGrid> <Text slot="errorMessage" /> </Calendar> </Dialog> </Popover> </DatePicker> If the date picker does not have a visible label, an `aria-label` or `aria-labelledby` prop must be passed instead to identify it to assistive technology. Note that most of this anatomy is shared with DateRangePicker, so you can reuse many components between them if you have both. ### Internationalization# To ensure the proper date and time format in RTL locales, `DateInput` must have `display` set to either `inline`, `inline-block`, or `block`. ### Concepts# `DatePicker` makes use of the following concepts: @internationalized/date Represent and manipulate dates and times in a locale-aware manner. Forms Validating and submitting form data, and integrating with form libraries. ### Composed components# A `DatePicker` uses the following components, which may also be used standalone or reused in other components. Label A label provides context for an input element. DateField A date field allows a user to enter and edit date values using a keyboard. Button A button allows a user to perform an action. Popover A popover displays content in context with a trigger element. Dialog A dialog is an overlay shown above other content in an application. Calendar A calendar allows a user to select a single date from a date grid. ## Examples# * * * DatePicker A DatePicker component styled with Tailwind CSS. ## Starter kits# * * * To help kick-start your project, we offer starter kits that include example implementations of all React Aria components with various styling solutions. All components are fully styled, including support for dark mode, high contrast mode, and all UI states. Each starter comes with a pre-configured Storybook that you can experiment with, or use as a starting point for your own component library. Vanilla CSS Download ZIP Preview Tailwind CSS Download ZIP Preview ## Reusable wrappers# * * * If you will use a DatePicker in multiple places in your app, you can wrap all of the pieces into a reusable component. This way, the DOM structure, styling code, and other logic are defined in a single place and reused everywhere to ensure consistency. This example wraps `DatePicker` and all of its children together into a single component which accepts a `label` prop, which is passed to the right place. It also shows how to use the `description` slot to render help text, and `FieldError` component to render validation errors. import type {DatePickerProps, DateValue, ValidationResult} from 'react-aria-components'; import {FieldError, Text} from 'react-aria-components'; interface MyDatePickerProps<T extends DateValue> extends DatePickerProps<T> { label?: string; description?: string; errorMessage?: string | ((validation: ValidationResult) => string); } function MyDatePicker<T extends DateValue>( { label, description, errorMessage, firstDayOfWeek, ...props }: MyDatePickerProps<T> ) { return ( <DatePicker {...props}> <Label>{label}</Label> <Group> <DateInput> {(segment) => <DateSegment segment={segment} />} </DateInput> <Button>▼</Button> </Group> {description && <Text slot="description">{description}</Text>} <FieldError>{errorMessage}</FieldError> <Popover> <Dialog> <Calendar firstDayOfWeek={firstDayOfWeek}> <header> <Button slot="previous">◀</Button> <Heading /> <Button slot="next">▶</Button> </header> <CalendarGrid> {(date) => <CalendarCell date={date} />} </CalendarGrid> </Calendar> </Dialog> </Popover> </DatePicker> ); } <MyDatePicker label="Event date" /> import type { DatePickerProps, DateValue, ValidationResult } from 'react-aria-components'; import {FieldError, Text} from 'react-aria-components'; interface MyDatePickerProps<T extends DateValue> extends DatePickerProps<T> { label?: string; description?: string; errorMessage?: | string | ((validation: ValidationResult) => string); } function MyDatePicker<T extends DateValue>( { label, description, errorMessage, firstDayOfWeek, ...props }: MyDatePickerProps<T> ) { return ( <DatePicker {...props}> <Label>{label}</Label> <Group> <DateInput> {(segment) => <DateSegment segment={segment} />} </DateInput> <Button>▼</Button> </Group> {description && ( <Text slot="description">{description}</Text> )} <FieldError>{errorMessage}</FieldError> <Popover> <Dialog> <Calendar firstDayOfWeek={firstDayOfWeek}> <header> <Button slot="previous">◀</Button> <Heading /> <Button slot="next">▶</Button> </header> <CalendarGrid> {(date) => <CalendarCell date={date} />} </CalendarGrid> </Calendar> </Dialog> </Popover> </DatePicker> ); } <MyDatePicker label="Event date" /> import type { DatePickerProps, DateValue, ValidationResult } from 'react-aria-components'; import { FieldError, Text } from 'react-aria-components'; interface MyDatePickerProps< T extends DateValue > extends DatePickerProps<T> { label?: string; description?: string; errorMessage?: | string | (( validation: ValidationResult ) => string); } function MyDatePicker< T extends DateValue >({ label, description, errorMessage, firstDayOfWeek, ...props }: MyDatePickerProps< T >) { return ( <DatePicker {...props} > <Label> {label} </Label> <Group> <DateInput> {(segment) => ( <DateSegment segment={segment} /> )} </DateInput> <Button> ▼ </Button> </Group> {description && ( <Text slot="description"> {description} </Text> )} <FieldError> {errorMessage} </FieldError> <Popover> <Dialog> <Calendar firstDayOfWeek={firstDayOfWeek} > <header> <Button slot="previous"> ◀ </Button> <Heading /> <Button slot="next"> ▶ </Button> </header> <CalendarGrid> {(date) => ( <CalendarCell date={date} /> )} </CalendarGrid> </Calendar> </Dialog> </Popover> </DatePicker> ); } <MyDatePicker label="Event date" /> Event date mm/dd/yyyy ▼ ## Value# * * * A `DatePicker` displays a placeholder by default. An initial, uncontrolled value can be provided to the `DatePicker` using the `defaultValue` prop. Alternatively, a controlled value can be provided using the `value` prop. Date values are provided using objects in the @internationalized/date package. This library handles correct international date manipulation across calendars, time zones, and other localization concerns. `DatePicker` supports values of the following types: * ` CalendarDate `– a date without any time components. May be parsed from a string representation using the` parseDate `function. Use this type to represent dates where the time is not important, such as a birthday or an all day calendar event. * ` CalendarDateTime `– a date with a time, but not in any specific time zone. May be parsed from a string representation using the` parseDateTime `function. Use this type to represent times that occur at the same local time regardless of the time zone, such as the time of New Years Eve fireworks which always occur at midnight. Most times are better stored as a `ZonedDateTime`. * ` ZonedDateTime `– a date with a time in a specific time zone. May be parsed from a string representation using the` parseZonedDateTime `,` parseAbsolute `, or` parseAbsoluteToLocal `functions. Use this type to represent an exact moment in time at a particular location on Earth. import {parseDate} from '@internationalized/date'; function Example() { let [value, setValue] = React.useState(parseDate('2020-02-03')); return ( <> <MyDatePicker label="Date (uncontrolled)" defaultValue={parseDate('2020-02-03')} /> <MyDatePicker label="Date (controlled)" value={value} onChange={setValue} /> </> ); } import {parseDate} from '@internationalized/date'; function Example() { let [value, setValue] = React.useState( parseDate('2020-02-03') ); return ( <> <MyDatePicker label="Date (uncontrolled)" defaultValue={parseDate('2020-02-03')} /> <MyDatePicker label="Date (controlled)" value={value} onChange={setValue} /> </> ); } import {parseDate} from '@internationalized/date'; function Example() { let [value, setValue] = React.useState( parseDate( '2020-02-03' ) ); return ( <> <MyDatePicker label="Date (uncontrolled)" defaultValue={parseDate( '2020-02-03' )} /> <MyDatePicker label="Date (controlled)" value={value} onChange={setValue} /> </> ); } Date (uncontrolled) 2/3/2020 ▼ Date (controlled) 2/3/2020 ▼ ### Time zones# `DatePicker` is time zone aware when a `ZonedDateTime` object is provided as the value. In this case, the time zone abbreviation is displayed, and time zone concerns such as daylight saving time are taken into account when the value is manipulated. In most cases, your data will come from and be sent to a server as an ISO 8601 formatted string. @internationalized/date includes functions for parsing strings in multiple formats into `ZonedDateTime` objects. Which format you use will depend on what information you need to store. * ` parseZonedDateTime `– This function parses a date with an explicit time zone and optional UTC offset attached (e.g. `"2021-11-07T00:45[America/Los_Angeles]"` or `"2021-11-07T00:45-07:00[America/Los_Angeles]"`). This format preserves the maximum amount of information. If the exact local time and time zone that a user selected is important, use this format. Storing the time zone and offset that was selected rather than converting to UTC ensures that the local time is correct regardless of daylight saving rule changes (e.g. if a locale abolishes DST). Examples where this applies include calendar events, reminders, and other times that occur in a particular location. * ` parseAbsolute `– This function parses an absolute date and time that occurs at the same instant at all locations on Earth. It can be represented in UTC (e.g. `"2021-11-07T07:45:00Z"`), or stored with a particular offset (e.g. `"2021-11-07T07:45:00-07:00"`). A time zone identifier, e.g. `America/Los_Angeles`, must be passed, and the result will be converted into that time zone. Absolute times are the best way to represent events that occurred in the past, or future events where an exact time is needed, regardless of time zone. * ` parseAbsoluteToLocal `– This function parses an absolute date and time into the current user's local time zone. It is a shortcut for `parseAbsolute`, and accepts the same formats. import {parseZonedDateTime} from '@internationalized/date'; <MyDatePicker label="Event date" defaultValue={parseZonedDateTime('2022-11-07T00:45[America/Los_Angeles]')} /> import {parseZonedDateTime} from '@internationalized/date'; <MyDatePicker label="Event date" defaultValue={parseZonedDateTime( '2022-11-07T00:45[America/Los_Angeles]' )} /> import {parseZonedDateTime} from '@internationalized/date'; <MyDatePicker label="Event date" defaultValue={parseZonedDateTime( '2022-11-07T00:45[America/Los_Angeles]' )} /> Event date 11/7/2022, 12:45 AM PST ▼ `DatePicker` displays times in the time zone included in the `ZonedDateTime` object. The above example is always displayed in Pacific Standard Time because the `America/Los_Angeles` time zone identifier is provided. @internationalized/date includes functions for converting dates between time zones, or parsing a date directly into a specific time zone or the user's local time zone, as shown below. import {parseAbsoluteToLocal} from '@internationalized/date'; <MyDatePicker label="Event date" defaultValue={parseAbsoluteToLocal('2021-11-07T07:45:00Z')} /> import {parseAbsoluteToLocal} from '@internationalized/date'; <MyDatePicker label="Event date" defaultValue={parseAbsoluteToLocal( '2021-11-07T07:45:00Z' )} /> import {parseAbsoluteToLocal} from '@internationalized/date'; <MyDatePicker label="Event date" defaultValue={parseAbsoluteToLocal( '2021-11-07T07:45:00Z' )} /> Event date 11/7/2021, 7:45 AM UTC ▼ ### Granularity# The `granularity` prop allows you to control the smallest unit that is displayed by `DatePicker`. By default, `CalendarDate` values are displayed with `"day"` granularity (year, month, and day), and `CalendarDateTime` and `ZonedDateTime` values are displayed with `"minute"` granularity. More granular time values can be displayed by setting the `granularity` prop to `"second"`. In addition, when a value with a time is provided but you wish to only display the date, you can set the granularity to `"day"`. This has no effect on the actual value (it still has a time component), only on what fields are displayed. In the following example, two DatePickers are synchronized with the same value, but display different granularities. function Example() { let [date, setDate] = React.useState( parseAbsoluteToLocal('2021-04-07T18:45:22Z') ); return ( <> <MyDatePicker label="Date and time" granularity="second" value={date} onChange={setDate} /> <MyDatePicker label="Date" granularity="day" value={date} onChange={setDate} /> </> ); } function Example() { let [date, setDate] = React.useState( parseAbsoluteToLocal('2021-04-07T18:45:22Z') ); return ( <> <MyDatePicker label="Date and time" granularity="second" value={date} onChange={setDate} /> <MyDatePicker label="Date" granularity="day" value={date} onChange={setDate} /> </> ); } function Example() { let [date, setDate] = React.useState( parseAbsoluteToLocal( '2021-04-07T18:45:22Z' ) ); return ( <> <MyDatePicker label="Date and time" granularity="second" value={date} onChange={setDate} /> <MyDatePicker label="Date" granularity="day" value={date} onChange={setDate} /> </> ); } Date and time 4/7/2021, 6:45:22 PM UTC ▼ Date 4/7/2021 ▼ If no `value` or `defaultValue` prop is passed, then the `granularity` prop also affects which type of value is emitted from the `onChange` event. Note that by default, time values will not have a time zone because none was supplied. You can override this by setting the `placeholderValue` prop explicitly. Values emitted from `onChange` will use the time zone of the placeholder value. import {now} from '@internationalized/date'; <MyDatePicker label="Event date" granularity="second" /> <MyDatePicker label="Event date" placeholderValue={now('America/New_York')} granularity="second" /> import {now} from '@internationalized/date'; <MyDatePicker label="Event date" granularity="second" /> <MyDatePicker label="Event date" placeholderValue={now('America/New_York')} granularity="second" /> import {now} from '@internationalized/date'; <MyDatePicker label="Event date" granularity="second" /> <MyDatePicker label="Event date" placeholderValue={now( 'America/New_York' )} granularity="second" /> Event date mm/dd/yyyy, ––:––:–– AM ▼ Event date mm/dd/yyyy, ––:––:–– AM EDT ▼ ### International calendars# `DatePicker` supports selecting dates in many calendar systems used around the world, including Gregorian, Hebrew, Indian, Islamic, Buddhist, and more. Dates are automatically displayed in the appropriate calendar system for the user's locale. The calendar system can be overridden using the Unicode calendar locale extension, passed to the I18nProvider component. Selected dates passed to `onChange` always use the same calendar system as the `value` or `defaultValue` prop. If no `value` or `defaultValue` is provided, then dates passed to `onChange` are always in the Gregorian calendar since this is the most commonly used. This means that even though the user selects dates in their local calendar system, applications are able to deal with dates from all users consistently. The below example displays a `DatePicker` in the Hindi language, using the Indian calendar. Dates emitted from `onChange` are in the Gregorian calendar. import {I18nProvider} from 'react-aria'; function Example() { let [date, setDate] = React.useState<DateValue | null>(null); return ( <I18nProvider locale="hi-IN-u-ca-indian"> <MyDatePicker label="Date" value={date} onChange={setDate} /> <p>Selected date: {date?.toString()}</p> </I18nProvider> ); } import {I18nProvider} from 'react-aria'; function Example() { let [date, setDate] = React.useState<DateValue | null>( null ); return ( <I18nProvider locale="hi-IN-u-ca-indian"> <MyDatePicker label="Date" value={date} onChange={setDate} /> <p>Selected date: {date?.toString()}</p> </I18nProvider> ); } import {I18nProvider} from 'react-aria'; function Example() { let [date, setDate] = React.useState< DateValue | null >(null); return ( <I18nProvider locale="hi-IN-u-ca-indian"> <MyDatePicker label="Date" value={date} onChange={setDate} /> <p> Selected date: {' '} {date ?.toString()} </p> </I18nProvider> ); } Date dd/mm/yyyy शक ▼ Selected date: ### HTML forms# DatePicker supports the `name` prop for integration with HTML forms. The value will be submitted to the server as an ISO 8601 formatted string according to the granularity of the value. For example, if the date picker allows selecting only a date then a string such as `"2023-02-03"` will be submitted, and if it allows selecting a time then a string such as `"2023-02-03T08:45:00"` will be submitted. See the Value section above for more details about the supported value types. <MyDatePicker label="Birth date" name="birthday" /> <MyDatePicker label="Birth date" name="birthday" /> <MyDatePicker label="Birth date" name="birthday" /> Birth date mm/dd/yyyy ▼ ## Events# * * * `DatePicker` accepts an `onChange` prop which is triggered whenever the date is edited by the user. The example below uses `onChange` to update a separate element with a formatted version of the date in the user's locale and local time zone. This is done by converting the date to a native JavaScript `Date` object to pass to the formatter. import {useDateFormatter} from 'react-aria'; import {getLocalTimeZone} from '@internationalized/date'; function Example() { let [date, setDate] = React.useState(parseDate('1985-07-03')); let formatter = useDateFormatter({ dateStyle: 'full' }); return ( <> <MyDatePicker label="Birth date" value={date} onChange={setDate} /> <p> Selected date:{' '} {date ? formatter.format(date.toDate(getLocalTimeZone())) : '--'} </p> </> ); } import {useDateFormatter} from 'react-aria'; import {getLocalTimeZone} from '@internationalized/date'; function Example() { let [date, setDate] = React.useState( parseDate('1985-07-03') ); let formatter = useDateFormatter({ dateStyle: 'full' }); return ( <> <MyDatePicker label="Birth date" value={date} onChange={setDate} /> <p> Selected date: {date ? formatter.format( date.toDate(getLocalTimeZone()) ) : '--'} </p> </> ); } import {useDateFormatter} from 'react-aria'; import {getLocalTimeZone} from '@internationalized/date'; function Example() { let [date, setDate] = React.useState( parseDate( '1985-07-03' ) ); let formatter = useDateFormatter({ dateStyle: 'full' }); return ( <> <MyDatePicker label="Birth date" value={date} onChange={setDate} /> <p> Selected date: {' '} {date ? formatter .format( date .toDate( getLocalTimeZone() ) ) : '--'} </p> </> ); } Birth date 7/3/1985 ▼ Selected date: Wednesday, July 3, 1985 ## Validation# * * * DatePicker supports the `isRequired` prop to ensure the user enters a value, as well as minimum and maximum values, and custom client and server-side validation. It can also be integrated with other form libraries. See the Forms guide to learn more. To display validation errors, add a `<FieldError>` element as a child of the DatePicker. This allows you to render error messages from all of the above sources with consistent custom styles. import {Form, FieldError} from 'react-aria-components'; <Form> <DatePicker name="date" isRequired> <Label>Appointment date</Label> <Group> <DateInput> {segment => <DateSegment segment={segment} />} </DateInput> <Button>▼</Button> </Group> <FieldError /> <Popover> <Dialog> <Calendar> <header> <Button slot="previous">◀</Button> <Heading /> <Button slot="next">▶</Button> </header> <CalendarGrid> {date => <CalendarCell date={date} />} </CalendarGrid> </Calendar> </Dialog> </Popover> </DatePicker> <Button type="submit">Submit</Button> </Form> import {Form, FieldError} from 'react-aria-components'; <Form> <DatePicker name="date" isRequired> <Label>Appointment date</Label> <Group> <DateInput> {segment => <DateSegment segment={segment} />} </DateInput> <Button>▼</Button> </Group> <FieldError /> <Popover> <Dialog> <Calendar> <header> <Button slot="previous">◀</Button> <Heading /> <Button slot="next">▶</Button> </header> <CalendarGrid> {date => <CalendarCell date={date} />} </CalendarGrid> </Calendar> </Dialog> </Popover> </DatePicker> <Button type="submit">Submit</Button> </Form> import { FieldError, Form } from 'react-aria-components'; <Form> <DatePicker name="date" isRequired > <Label> Appointment date </Label> <Group> <DateInput> {(segment) => ( <DateSegment segment={segment} /> )} </DateInput> <Button> ▼ </Button> </Group> <FieldError /> <Popover> <Dialog> <Calendar> <header> <Button slot="previous"> ◀ </Button> <Heading /> <Button slot="next"> ▶ </Button> </header> <CalendarGrid> {(date) => ( <CalendarCell date={date} /> )} </CalendarGrid> </Calendar> </Dialog> </Popover> </DatePicker> <Button type="submit"> Submit </Button> </Form> Appointment date mm/dd/yyyy ▼ Submit Show CSS .react-aria-DatePicker { &[data-invalid] { .react-aria-DateInput:after { content: '🚫' / ''; content: '🚫'; alt: ' '; flex: 1; text-align: end; } } .react-aria-FieldError { font-size: 12px; color: var(--invalid-color); } } .react-aria-DatePicker { &[data-invalid] { .react-aria-DateInput:after { content: '🚫' / ''; content: '🚫'; alt: ' '; flex: 1; text-align: end; } } .react-aria-FieldError { font-size: 12px; color: var(--invalid-color); } } .react-aria-DatePicker { &[data-invalid] { .react-aria-DateInput:after { content: '🚫' / ''; content: '🚫'; alt: ' '; flex: 1; text-align: end; } } .react-aria-FieldError { font-size: 12px; color: var(--invalid-color); } } By default, `FieldError` displays default validation messages provided by the browser. See Customizing error messages in the Forms guide to learn how to provide your own custom errors. ### Minimum and maximum values# The `minValue` and `maxValue` props can also be used to ensure the value is within a specific range. This example only accepts dates after today. import {today} from '@internationalized/date'; <Form> <MyDatePicker label="Appointment date" minValue={today(getLocalTimeZone())} defaultValue={parseDate('2022-02-03')} /> <Button type="submit">Submit</Button> </Form> import {today} from '@internationalized/date'; <Form> <MyDatePicker label="Appointment date" minValue={today(getLocalTimeZone())} defaultValue={parseDate('2022-02-03')} /> <Button type="submit">Submit</Button> </Form> import {today} from '@internationalized/date'; <Form> <MyDatePicker label="Appointment date" minValue={today( getLocalTimeZone() )} defaultValue={parseDate( '2022-02-03' )} /> <Button type="submit"> Submit </Button> </Form> Appointment date 2/3/2022 ▼ Submit ### Unavailable dates# `DatePicker` supports marking certain dates as _unavailable_. These dates remain focusable with the keyboard in the calendar so that navigation is consistent, but cannot be selected by the user. When an unavailable date is entered into the date field, it is marked as invalid. The `isDateUnavailable` prop accepts a callback that is called to evaluate whether each visible date is unavailable. This example includes multiple unavailable date ranges, e.g. dates when no appointments are available. In addition, all weekends are unavailable. The `minValue` prop is also used to prevent selecting dates before today. import {useLocale} from 'react-aria'; import {isWeekend, today} from '@internationalized/date'; function Example() { let now = today(getLocalTimeZone()); let disabledRanges = [ [now, now.add({ days: 5 })], [now.add({ days: 14 }), now.add({ days: 16 })], [now.add({ days: 23 }), now.add({ days: 24 })] ]; let { locale } = useLocale(); let isDateUnavailable = (date: DateValue) => isWeekend(date, locale) || disabledRanges.some((interval) => date.compare(interval[0]) >= 0 && date.compare(interval[1]) <= 0 ); return ( <MyDatePicker label="Appointment date" minValue={today(getLocalTimeZone())} isDateUnavailable={isDateUnavailable} /> ); } import {useLocale} from 'react-aria'; import {isWeekend, today} from '@internationalized/date'; function Example() { let now = today(getLocalTimeZone()); let disabledRanges = [ [now, now.add({ days: 5 })], [now.add({ days: 14 }), now.add({ days: 16 })], [now.add({ days: 23 }), now.add({ days: 24 })] ]; let { locale } = useLocale(); let isDateUnavailable = (date: DateValue) => isWeekend(date, locale) || disabledRanges.some((interval) => date.compare(interval[0]) >= 0 && date.compare(interval[1]) <= 0 ); return ( <MyDatePicker label="Appointment date" minValue={today(getLocalTimeZone())} isDateUnavailable={isDateUnavailable} /> ); } import {useLocale} from 'react-aria'; import { isWeekend, today } from '@internationalized/date'; function Example() { let now = today( getLocalTimeZone() ); let disabledRanges = [ [ now, now.add({ days: 5 }) ], [ now.add({ days: 14 }), now.add({ days: 16 }) ], [ now.add({ days: 23 }), now.add({ days: 24 }) ] ]; let { locale } = useLocale(); let isDateUnavailable = (date: DateValue) => isWeekend( date, locale ) || disabledRanges .some(( interval ) => date.compare( interval[0] ) >= 0 && date.compare( interval[1] ) <= 0 ); return ( <MyDatePicker label="Appointment date" minValue={today( getLocalTimeZone() )} isDateUnavailable={isDateUnavailable} /> ); } Appointment date mm/dd/yyyy ▼ ### Custom validation# The `validate` function can be used to perform custom validation logic. It receives the current date value, and should return a string or array of strings representing one or more error messages if the value is invalid. This example validates that the selected date is a weekday and not a weekend according to the current locale. import {isWeekend} from '@internationalized/date'; import {useLocale} from 'react-aria'; function Example() { let { locale } = useLocale(); return ( <Form> <MyDatePicker label="Appointment date" validate={(date) => date && isWeekend(date, locale) ? 'We are closed on weekends.' : null} defaultValue={parseDate('2023-10-28')} /> <Button type="submit">Submit</Button> </Form> ); } import {isWeekend} from '@internationalized/date'; import {useLocale} from 'react-aria'; function Example() { let { locale } = useLocale(); return ( <Form> <MyDatePicker label="Appointment date" validate={(date) => date && isWeekend(date, locale) ? 'We are closed on weekends.' : null} defaultValue={parseDate('2023-10-28')} /> <Button type="submit">Submit</Button> </Form> ); } import {isWeekend} from '@internationalized/date'; import {useLocale} from 'react-aria'; function Example() { let { locale } = useLocale(); return ( <Form> <MyDatePicker label="Appointment date" validate={(date) => date && isWeekend( date, locale ) ? 'We are closed on weekends.' : null} defaultValue={parseDate( '2023-10-28' )} /> <Button type="submit"> Submit </Button> </Form> ); } Appointment date 10/28/2023 ▼ Submit ### Description# The `description` slot can be used to associate additional help text with a date picker. <DatePicker granularity="hour"> <Label>Appointment time</Label> <Group> <DateInput> {segment => <DateSegment segment={segment} />} </DateInput> <Button>▼</Button> </Group> <Text slot="description">Please select a weekday between 9 AM and 5 PM.</Text> <Popover> <Dialog> <Calendar> <header> <Button slot="previous">◀</Button> <Heading /> <Button slot="next">▶</Button> </header> <CalendarGrid> {date => <CalendarCell date={date} />} </CalendarGrid> </Calendar> </Dialog> </Popover> </DatePicker> <DatePicker granularity="hour"> <Label>Appointment time</Label> <Group> <DateInput> {(segment) => <DateSegment segment={segment} />} </DateInput> <Button>▼</Button> </Group> <Text slot="description"> Please select a weekday between 9 AM and 5 PM. </Text> <Popover> <Dialog> <Calendar> <header> <Button slot="previous">◀</Button> <Heading /> <Button slot="next">▶</Button> </header> <CalendarGrid> {(date) => <CalendarCell date={date} />} </CalendarGrid> </Calendar> </Dialog> </Popover> </DatePicker> <DatePicker granularity="hour"> <Label> Appointment time </Label> <Group> <DateInput> {(segment) => ( <DateSegment segment={segment} /> )} </DateInput> <Button>▼</Button> </Group> <Text slot="description"> Please select a weekday between 9 AM and 5 PM. </Text> <Popover> <Dialog> <Calendar> <header> <Button slot="previous"> ◀ </Button> <Heading /> <Button slot="next"> ▶ </Button> </header> <CalendarGrid> {(date) => ( <CalendarCell date={date} /> )} </CalendarGrid> </Calendar> </Dialog> </Popover> </DatePicker> Appointment time mm/dd/yyyy, –– AM ▼ Please select a weekday between 9 AM and 5 PM. Show CSS .react-aria-DatePicker { [slot=description] { font-size: 12px; } } .react-aria-DatePicker { [slot=description] { font-size: 12px; } } .react-aria-DatePicker { [slot=description] { font-size: 12px; } } ## Format options# * * * ### Placeholder value# When no value is set, a placeholder is shown. The format of the placeholder is influenced by the `granularity` and `placeholderValue` props. `placeholderValue` also controls the default values of each segment when the user first interacts with them, e.g. using the up and down arrow keys, as well as the default month shown in the calendar popover. By default, the `placeholderValue` is the current date at midnight, but you can set it to a more appropriate value if needed. import {CalendarDate} from '@internationalized/date'; <MyDatePicker label="Birth date" placeholderValue={new CalendarDate(1980, 1, 1)} /> import {CalendarDate} from '@internationalized/date'; <MyDatePicker label="Birth date" placeholderValue={new CalendarDate(1980, 1, 1)} /> import {CalendarDate} from '@internationalized/date'; <MyDatePicker label="Birth date" placeholderValue={new CalendarDate( 1980, 1, 1 )} /> Birth date mm/dd/yyyy ▼ ### Hide time zone# When a `ZonedDateTime` object is provided as the value to `DatePicker`, the time zone abbreviation is displayed by default. However, if this is displayed elsewhere or implicit based on the usecase, it can be hidden using the `hideTimeZone` option. <MyDatePicker label="Appointment time" defaultValue={parseZonedDateTime('2022-11-07T10:45[America/Los_Angeles]')} hideTimeZone /> <MyDatePicker label="Appointment time" defaultValue={parseZonedDateTime( '2022-11-07T10:45[America/Los_Angeles]' )} hideTimeZone /> <MyDatePicker label="Appointment time" defaultValue={parseZonedDateTime( '2022-11-07T10:45[America/Los_Angeles]' )} hideTimeZone /> Appointment time 11/7/2022, 10:45 AM ▼ ### Hour cycle# By default, `DatePicker` displays times in either 12 or 24 hour hour format depending on the user's locale. However, this can be overridden using the `hourCycle` prop if needed for a specific usecase. This example forces `DatePicker` to use 24-hour time, regardless of the locale. <MyDatePicker label="Appointment time" granularity="minute" hourCycle={24} /> <MyDatePicker label="Appointment time" granularity="minute" hourCycle={24} /> <MyDatePicker label="Appointment time" granularity="minute" hourCycle={24} /> Appointment time mm/dd/yyyy, ––:–– ▼ ## Custom first day of week# * * * By default, the first day of the week is automatically set based on the current locale. This can be changed by setting the `firstDayOfWeek` prop to `'sun'`, `'mon'`, `'tue'`, `'wed'`, `'thu'`, `'fri'`, or `'sat'`. <MyDatePicker aria-label="Appointment time" firstDayOfWeek="mon" /> <MyDatePicker aria-label="Appointment time" firstDayOfWeek="mon" /> <MyDatePicker aria-label="Appointment time" firstDayOfWeek="mon" /> mm/dd/yyyy ▼ ## Props# * * * ### DatePicker# | Name | Type | Default | Description | | --- | --- | --- | --- | | `pageBehavior` | ` PageBehavior ` | `visible` | Controls the behavior of paging. Pagination either works by advancing the visible page by visibleDuration (default) or one unit of visibleDuration. | | `firstDayOfWeek` | `'sun' | 'mon' | 'tue' | 'wed' | 'thu' | 'fri' | 'sat'` | — | The day that starts the week. | | `minValue` | ` DateValue | null` | — | The minimum allowed date that a user may select. | | `maxValue` | ` DateValue | null` | — | The maximum allowed date that a user may select. | | `isDateUnavailable` | `( (date: DateValue )) => boolean` | — | Callback that is called for each date of the calendar. If it returns true, then the date is unavailable. | | `placeholderValue` | ` DateValue | null` | — | A placeholder date that influences the format of the placeholder shown when no value is selected. Defaults to today's date at midnight. | | `hourCycle` | `12 | 24` | — | Whether to display the time in 12 or 24 hour format. By default, this is determined by the user's locale. | | `granularity` | ` Granularity ` | — | Determines the smallest unit that is displayed in the date picker. By default, this is `"day"` for dates, and `"minute"` for times. | | `hideTimeZone` | `boolean` | `false` | Whether to hide the time zone abbreviation. | | `shouldForceLeadingZeros` | `boolean` | — | Whether to always show leading zeros in the month, day, and hour fields. By default, this is determined by the user's locale. | | `isDisabled` | `boolean` | — | Whether the input is disabled. | | `isReadOnly` | `boolean` | — | Whether the input can be selected but not changed by the user. | | `isRequired` | `boolean` | — | Whether user input is required on the input before form submission. | | `isInvalid` | `boolean` | — | Whether the input value is invalid. | | `validate` | `( (value: MappedDateValue < DateValue > )) => ValidationError | true | null | undefined` | — | A function that returns an error message if a given value is invalid. Validation errors are displayed to the user when the form is submitted if `validationBehavior="native"`. For realtime validation, use the `isInvalid` prop instead. | | `autoFocus` | `boolean` | — | Whether the element should receive focus on render. | | `isOpen` | `boolean` | — | Whether the overlay is open by default (controlled). | | `defaultOpen` | `boolean` | — | Whether the overlay is open by default (uncontrolled). | | `value` | ` DateValue | null` | — | The current value (controlled). | | `defaultValue` | ` DateValue | null` | — | The default value (uncontrolled). | | `name` | `string` | — | The name of the input element, used when submitting an HTML form. See MDN. | | `shouldCloseOnSelect` | `boolean | () => boolean` | `true` | Determines whether the date picker popover should close automatically when a date is selected. | | `validationBehavior` | `'native' | 'aria'` | `'native'` | Whether to use native HTML form validation to prevent form submission when the value is missing or invalid, or mark the field as required or invalid via ARIA. | | `children` | `ReactNode | ( (values: DatePickerRenderProps & & { defaultChildren: ReactNode | | undefined } )) => ReactNode` | — | The children of the component. A function may be provided to alter the children based on component state. | | `className` | `string | ( (values: DatePickerRenderProps & & { defaultClassName: string | | undefined } )) => string` | — | The CSS className for the element. A function may be provided to compute the class based on component state. | | `style` | `CSSProperties | ( (values: DatePickerRenderProps & & { defaultStyle: CSSProperties } )) => CSSProperties | undefined` | — | The inline style for the element. A function may be provided to compute the style based on component state. | Events | Name | Type | Description | | --- | --- | --- | | `onFocus` | `( (e: FocusEvent<Target> )) => void` | Handler that is called when the element receives focus. | | `onBlur` | `( (e: FocusEvent<Target> )) => void` | Handler that is called when the element loses focus. | | `onFocusChange` | `( (isFocused: boolean )) => void` | Handler that is called when the element's focus status changes. | | `onKeyDown` | `( (e: KeyboardEvent )) => void` | Handler that is called when a key is pressed. | | `onKeyUp` | `( (e: KeyboardEvent )) => void` | Handler that is called when a key is released. | | `onOpenChange` | `( (isOpen: boolean )) => void` | Handler that is called when the overlay's open state changes. | | `onChange` | `( (value: MappedDateValue < DateValue > | | null )) => void` | Handler that is called when the value changes. | Layout | Name | Type | Description | | --- | --- | --- | | `slot` | `string | null` | A slot name for the component. Slots allow the component to receive props from a parent component. An explicit `null` value indicates that the local props completely override all props received from a parent. | Accessibility | Name | Type | Description | | --- | --- | --- | | `id` | `string` | The element's unique identifier. See MDN. | | `aria-label` | `string` | Defines a string value that labels the current element. | | `aria-labelledby` | `string` | Identifies the element (or elements) that labels the current element. | | `aria-describedby` | `string` | Identifies the element (or elements) that describes the object. | | `aria-details` | `string` | Identifies the element (or elements) that provide a detailed, extended description for the object. | ### Label# A `<Label>` accepts all HTML attributes. ### Group# A `<Group>` accepts all HTML attributes. ### DateInput# The `<DateInput>` component renders a group of date segments. It accepts a function as its `children`, which is called to render a `<DateSegment>` for each segment. Show props | Name | Type | Description | | --- | --- | --- | | `children` | `( (segment: DateSegment )) => ReactElement` | | | `className` | `string | ( (values: DateInputRenderProps & & { defaultClassName: string | | undefined } )) => string` | The CSS className for the element. A function may be provided to compute the class based on component state. | | `style` | `CSSProperties | ( (values: DateInputRenderProps & & { defaultStyle: CSSProperties } )) => CSSProperties | undefined` | The inline style for the element. A function may be provided to compute the style based on component state. | Layout | Name | Type | Description | | --- | --- | --- | | `slot` | `string | null` | A slot name for the component. Slots allow the component to receive props from a parent component. An explicit `null` value indicates that the local props completely override all props received from a parent. | ### DateSegment# The `<DateSegment>` component renders an individual segment. Show props | Name | Type | Description | | --- | --- | --- | | `segment` | ` DateSegment ` | | | `children` | `ReactNode | ( (values: DateSegmentRenderProps & & { defaultChildren: ReactNode | | undefined } )) => ReactNode` | The children of the component. A function may be provided to alter the children based on component state. | | `className` | `string | ( (values: DateSegmentRenderProps & & { defaultClassName: string | | undefined } )) => string` | The CSS className for the element. A function may be provided to compute the class based on component state. | | `style` | `CSSProperties | ( (values: DateSegmentRenderProps & & { defaultStyle: CSSProperties } )) => CSSProperties | undefined` | The inline style for the element. A function may be provided to compute the style based on component state. | Events | Name | Type | Description | | --- | --- | --- | | `onHoverStart` | `( (e: HoverEvent )) => void` | Handler that is called when a hover interaction starts. | | `onHoverEnd` | `( (e: HoverEvent )) => void` | Handler that is called when a hover interaction ends. | | `onHoverChange` | `( (isHovering: boolean )) => void` | Handler that is called when the hover state changes. | ### Button# A `<Button>` accepts its contents as `children`. Other props such as `onPress` and `isDisabled` will be set by the `DatePicker` or `Calendar`. Show props | Name | Type | Default | Description | | --- | --- | --- | --- | | `form` | `string` | — | The `<form>` element to associate the button with. The value of this attribute must be the id of a `<form>` in the same document. | | `formAction` | `string` | — | The URL that processes the information submitted by the button. Overrides the action attribute of the button's form owner. | | `formEncType` | `string` | — | Indicates how to encode the form data that is submitted. | | `formMethod` | `string` | — | Indicates the HTTP method used to submit the form. | | `formNoValidate` | `boolean` | — | Indicates that the form is not to be validated when it is submitted. | | `formTarget` | `string` | — | Overrides the target attribute of the button's form owner. | | `name` | `string` | — | Submitted as a pair with the button's value as part of the form data. | | `value` | `string` | — | The value associated with the button's name when it's submitted with the form data. | | `isPending` | `boolean` | — | Whether the button is in a pending state. This disables press and hover events while retaining focusability, and announces the pending state to screen readers. | | `isDisabled` | `boolean` | — | Whether the button is disabled. | | `autoFocus` | `boolean` | — | Whether the element should receive focus on render. | | `type` | `'button' | 'submit' | 'reset'` | `'button'` | The behavior of the button when used in an HTML form. | | `children` | `ReactNode | ( (values: ButtonRenderProps & & { defaultChildren: ReactNode | | undefined } )) => ReactNode` | — | The children of the component. A function may be provided to alter the children based on component state. | | `className` | `string | ( (values: ButtonRenderProps & & { defaultClassName: string | | undefined } )) => string` | — | The CSS className for the element. A function may be provided to compute the class based on component state. | | `style` | `CSSProperties | ( (values: ButtonRenderProps & & { defaultStyle: CSSProperties } )) => CSSProperties | undefined` | — | The inline style for the element. A function may be provided to compute the style based on component state. | Events | Name | Type | Description | | --- | --- | --- | | `onPress` | `( (e: PressEvent )) => void` | Handler that is called when the press is released over the target. | | `onPressStart` | `( (e: PressEvent )) => void` | Handler that is called when a press interaction starts. | | `onPressEnd` | `( (e: PressEvent )) => void` | Handler that is called when a press interaction ends, either over the target or when the pointer leaves the target. | | `onPressChange` | `( (isPressed: boolean )) => void` | Handler that is called when the press state changes. | | `onPressUp` | `( (e: PressEvent )) => void` | Handler that is called when a press is released over the target, regardless of whether it started on the target or not. | | `onClick` | `( (e: MouseEvent<FocusableElement> )) => void` | **Not recommended – use `onPress` instead.** `onClick` is an alias for `onPress` provided for compatibility with other libraries. `onPress` provides additional event details for non-mouse interactions. | | `onFocus` | `( (e: FocusEvent<Target> )) => void` | Handler that is called when the element receives focus. | | `onBlur` | `( (e: FocusEvent<Target> )) => void` | Handler that is called when the element loses focus. | | `onFocusChange` | `( (isFocused: boolean )) => void` | Handler that is called when the element's focus status changes. | | `onKeyDown` | `( (e: KeyboardEvent )) => void` | Handler that is called when a key is pressed. | | `onKeyUp` | `( (e: KeyboardEvent )) => void` | Handler that is called when a key is released. | | `onHoverStart` | `( (e: HoverEvent )) => void` | Handler that is called when a hover interaction starts. | | `onHoverEnd` | `( (e: HoverEvent )) => void` | Handler that is called when a hover interaction ends. | | `onHoverChange` | `( (isHovering: boolean )) => void` | Handler that is called when the hover state changes. | Layout | Name | Type | Description | | --- | --- | --- | | `slot` | `string | null` | A slot name for the component. Slots allow the component to receive props from a parent component. An explicit `null` value indicates that the local props completely override all props received from a parent. | Accessibility | Name | Type | Description | | --- | --- | --- | | `id` | `string` | The element's unique identifier. See MDN. | | `excludeFromTabOrder` | `boolean` | Whether to exclude the element from the sequential tab order. If true, the element will not be focusable via the keyboard by tabbing. This should be avoided except in rare scenarios where an alternative means of accessing the element or its functionality via the keyboard is available. | | `preventFocusOnPress` | `boolean` | Whether to prevent focus from moving to the button when pressing it. Caution, this can make the button inaccessible and should only be used when alternative keyboard interaction is provided, such as ComboBox's MenuTrigger or a NumberField's increment/decrement control. | | `aria-expanded` | `boolean | 'true' | 'false'` | Indicates whether the element, or another grouping element it controls, is currently expanded or collapsed. | | `aria-haspopup` | `boolean | 'menu' | 'listbox' | 'tree' | 'grid' | 'dialog' | 'true' | 'false'` | Indicates the availability and type of interactive popup element, such as menu or dialog, that can be triggered by an element. | | `aria-controls` | `string` | Identifies the element (or elements) whose contents or presence are controlled by the current element. | | `aria-pressed` | `boolean | 'true' | 'false' | 'mixed'` | Indicates the current "pressed" state of toggle buttons. | | `aria-current` | `boolean | 'true' | 'false' | 'page' | 'step' | 'location' | 'date' | 'time'` | Indicates whether this element represents the current item within a container or set of related elements. | | `aria-label` | `string` | Defines a string value that labels the current element. | | `aria-labelledby` | `string` | Identifies the element (or elements) that labels the current element. | | `aria-describedby` | `string` | Identifies the element (or elements) that describes the object. | | `aria-details` | `string` | Identifies the element (or elements) that provide a detailed, extended description for the object. | ### FieldError# A `<FieldError>` displays validation errors. Show props | Name | Type | Description | | --- | --- | --- | | `children` | `ReactNode | ( (values: FieldErrorRenderProps & & { defaultChildren: ReactNode | | undefined } )) => ReactNode` | The children of the component. A function may be provided to alter the children based on component state. | | `className` | `string | ( (values: FieldErrorRenderProps & & { defaultClassName: string | | undefined } )) => string` | The CSS className for the element. A function may be provided to compute the class based on component state. | | `style` | `CSSProperties | ( (values: FieldErrorRenderProps & & { defaultStyle: CSSProperties } )) => CSSProperties | undefined` | The inline style for the element. A function may be provided to compute the style based on component state. | Accessibility | Name | Type | Description | | --- | --- | --- | | `id` | `string` | The element's unique identifier. See MDN. | ### Popover# A `<Popover>` is an overlay to hold the `<Calendar>`, positioned relative to the `<Group>`. By default, it has a `placement` of `bottom start` within a `<DatePicker>`, but this and other positioning properties may be customized. Show props | Name | Type | Default | Description | | --- | --- | --- | --- | | `trigger` | `string` | — | The name of the component that triggered the popover. This is reflected on the element as the `data-trigger` attribute, and can be used to provide specific styles for the popover depending on which element triggered it. | | `triggerRef` | ` RefObject <Element | null>` | — | The ref for the element which the popover positions itself with respect to. When used within a trigger component such as DialogTrigger, MenuTrigger, Select, etc., this is set automatically. It is only required when used standalone. | | `isEntering` | `boolean` | — | Whether the popover is currently performing an entry animation. | | `isExiting` | `boolean` | — | Whether the popover is currently performing an exit animation. | | `offset` | `number` | `8` | The additional offset applied along the main axis between the element and its anchor element. | | `placement` | ` Placement ` | `'bottom'` | The placement of the element with respect to its anchor element. | | `containerPadding` | `number` | `12` | The placement padding that should be applied between the element and its surrounding container. | | `crossOffset` | `number` | `0` | The additional offset applied along the cross axis between the element and its anchor element. | | `shouldFlip` | `boolean` | `true` | Whether the element should flip its orientation (e.g. top to bottom or left to right) when there is insufficient room for it to render completely. | | `isNonModal` | `boolean` | — | Whether the popover is non-modal, i.e. elements outside the popover may be interacted with by assistive technologies. Most popovers should not use this option as it may negatively impact the screen reader experience. Only use with components such as combobox, which are designed to handle this situation carefully. | | `isKeyboardDismissDisabled` | `boolean` | `false` | Whether pressing the escape key to close the popover should be disabled. Most popovers should not use this option. When set to true, an alternative way to close the popover with a keyboard must be provided. | | `shouldCloseOnInteractOutside` | `( (element: Element )) => boolean` | — | When user interacts with the argument element outside of the popover ref, return true if onClose should be called. This gives you a chance to filter out interaction with elements that should not dismiss the popover. By default, onClose will always be called on interaction outside the popover ref. | | `boundaryElement` | `Element` | `document.body` | Element that that serves as the positioning boundary. | | `scrollRef` | ` RefObject <Element | null>` | `overlayRef` | A ref for the scrollable region within the overlay. | | `shouldUpdatePosition` | `boolean` | `true` | Whether the overlay should update its position automatically. | | `arrowBoundaryOffset` | `number` | `0` | The minimum distance the arrow's edge should be from the edge of the overlay element. | | `isOpen` | `boolean` | — | Whether the overlay is open by default (controlled). | | `defaultOpen` | `boolean` | — | Whether the overlay is open by default (uncontrolled). | | `children` | `ReactNode | ( (values: PopoverRenderProps & & { defaultChildren: ReactNode | | undefined } )) => ReactNode` | — | The children of the component. A function may be provided to alter the children based on component state. | | `className` | `string | ( (values: PopoverRenderProps & & { defaultClassName: string | | undefined } )) => string` | — | The CSS className for the element. A function may be provided to compute the class based on component state. | | `style` | `CSSProperties | ( (values: PopoverRenderProps & & { defaultStyle: CSSProperties } )) => CSSProperties | undefined` | — | The inline style for the element. A function may be provided to compute the style based on component state. | Events | Name | Type | Description | | --- | --- | --- | | `onOpenChange` | `( (isOpen: boolean )) => void` | Handler that is called when the overlay's open state changes. | Layout | Name | Type | Description | | --- | --- | --- | | `slot` | `string | null` | A slot name for the component. Slots allow the component to receive props from a parent component. An explicit `null` value indicates that the local props completely override all props received from a parent. | Sizing | Name | Type | Description | | --- | --- | --- | | `maxHeight` | `number` | The maxHeight specified for the overlay element. By default, it will take all space up to the current viewport height. | Accessibility | Name | Type | Description | | --- | --- | --- | | `aria-label` | `string` | Defines a string value that labels the current element. | | `aria-labelledby` | `string` | Identifies the element (or elements) that labels the current element. | | `aria-describedby` | `string` | Identifies the element (or elements) that describes the object. | | `aria-details` | `string` | Identifies the element (or elements) that provide a detailed, extended description for the object. | ### Dialog# A `<Dialog>` is placed within a `<Popover>` to hold the `<Calendar>` for a DatePicker. Show props | Name | Type | Description | | --- | --- | --- | | `children` | `ReactNode | ( (opts: DialogRenderProps )) => ReactNode` | Children of the dialog. A function may be provided to access a function to close the dialog. | | `className` | `string` | The CSS className for the element. | | `style` | `CSSProperties` | The inline style for the element. | Layout | Name | Type | Description | | --- | --- | --- | | `slot` | `string | null` | A slot name for the component. Slots allow the component to receive props from a parent component. An explicit `null` value indicates that the local props completely override all props received from a parent. | Accessibility | Name | Type | Default | Description | | --- | --- | --- | --- | | `role` | `'dialog' | 'alertdialog'` | `'dialog'` | The accessibility role for the dialog. | | `id` | `string` | — | The element's unique identifier. See MDN. | | `aria-label` | `string` | — | Defines a string value that labels the current element. | | `aria-labelledby` | `string` | — | Identifies the element (or elements) that labels the current element. | | `aria-describedby` | `string` | — | Identifies the element (or elements) that describes the object. | | `aria-details` | `string` | — | Identifies the element (or elements) that provide a detailed, extended description for the object. | ### Calendar# A `<Calendar>` accepts one or more month grids as children, along with previous and next buttons to navigate forward and backward. Show props | Name | Type | Default | Description | | --- | --- | --- | --- | | `visibleDuration` | ` DateDuration ` | `{months: 1}` | The amount of days that will be displayed at once. This affects how pagination works. | | `createCalendar` | `( (identifier: CalendarIdentifier )) => Calendar ` | — | A function to create a new Calendar object for a given calendar identifier. If not provided, the `createCalendar` function from `@internationalized/date` will be used. | | `minValue` | ` DateValue | null` | — | The minimum allowed date that a user may select. | | `maxValue` | ` DateValue | null` | — | The maximum allowed date that a user may select. | | `isDateUnavailable` | `( (date: DateValue )) => boolean` | — | Callback that is called for each date of the calendar. If it returns true, then the date is unavailable. | | `isDisabled` | `boolean` | `false` | Whether the calendar is disabled. | | `isReadOnly` | `boolean` | `false` | Whether the calendar value is immutable. | | `autoFocus` | `boolean` | `false` | Whether to automatically focus the calendar when it mounts. | | `focusedValue` | ` DateValue | null` | — | Controls the currently focused date within the calendar. | | `defaultFocusedValue` | ` DateValue | null` | — | The date that is focused when the calendar first mounts (uncountrolled). | | `isInvalid` | `boolean` | — | Whether the current selection is invalid according to application logic. | | `pageBehavior` | ` PageBehavior ` | `visible` | Controls the behavior of paging. Pagination either works by advancing the visible page by visibleDuration (default) or one unit of visibleDuration. | | `firstDayOfWeek` | `'sun' | 'mon' | 'tue' | 'wed' | 'thu' | 'fri' | 'sat'` | — | The day that starts the week. | | `value` | ` DateValue | null` | — | The current value (controlled). | | `defaultValue` | ` DateValue | null` | — | The default value (uncontrolled). | | `children` | `ReactNode | ( (values: CalendarRenderProps & & { defaultChildren: ReactNode | | undefined } )) => ReactNode` | — | The children of the component. A function may be provided to alter the children based on component state. | | `className` | `string | ( (values: CalendarRenderProps & & { defaultClassName: string | | undefined } )) => string` | — | The CSS className for the element. A function may be provided to compute the class based on component state. | | `style` | `CSSProperties | ( (values: CalendarRenderProps & & { defaultStyle: CSSProperties } )) => CSSProperties | undefined` | — | The inline style for the element. A function may be provided to compute the style based on component state. | Events | Name | Type | Description | | --- | --- | --- | | `onFocusChange` | `( (date: CalendarDate )) => void` | Handler that is called when the focused date changes. | | `onChange` | `( (value: MappedDateValue < DateValue > )) => void` | Handler that is called when the value changes. | Layout | Name | Type | Description | | --- | --- | --- | | `slot` | `string | null` | A slot name for the component. Slots allow the component to receive props from a parent component. An explicit `null` value indicates that the local props completely override all props received from a parent. | Accessibility | Name | Type | Description | | --- | --- | --- | | `id` | `string` | The element's unique identifier. See MDN. | | `aria-label` | `string` | Defines a string value that labels the current element. | | `aria-labelledby` | `string` | Identifies the element (or elements) that labels the current element. | | `aria-describedby` | `string` | Identifies the element (or elements) that describes the object. | | `aria-details` | `string` | Identifies the element (or elements) that provide a detailed, extended description for the object. | ### CalendarGrid# A `<CalendarGrid>` renders an individual month within a `<Calendar>`. It accepts a function as its `children`, which is called to render a `<CalendarCell>` for each date. This renders a default `<CalendarGridHeader>`, which displays the weekday names in the column headers. This can be customized by providing a `<CalendarGridHeader>` and `<CalendarGridBody>` as children instead of a function. Show props | Name | Type | Default | Description | | --- | --- | --- | --- | | `children` | `ReactElement | ReactElement[] | ( (date: CalendarDate )) => ReactElement` | — | Either a function to render calendar cells for each date in the month, or children containing a `<CalendarGridHeader>`` and`<CalendarGridBody>\` when additional customization is needed. | | `offset` | ` DateDuration ` | — | An offset from the beginning of the visible date range that this CalendarGrid should display. Useful when displaying more than one month at a time. | | `weekdayStyle` | `'narrow' | 'short' | 'long'` | `"narrow"` | The style of weekday names to display in the calendar grid header, e.g. single letter, abbreviation, or full day name. | | `className` | `string` | — | The CSS className for the element. | | `style` | `CSSProperties` | — | The inline style for the element. | ### CalendarGridHeader# A `<CalendarGridHeader>` renders the header within a `<CalendarGrid>`, displaying a list of weekday names. It accepts a function as its `children`, which is called with a day name abbreviation to render a `<CalendarHeaderCell>` for each column header. Show props | Name | Type | Description | | --- | --- | --- | | `children` | `( (day: string )) => ReactElement` | A function to render a `<CalendarHeaderCell>` for a weekday name. | | `className` | `string` | The CSS className for the element. | | `style` | `CSSProperties` | The inline style for the element. | ### CalendarHeaderCell# A `<CalendarHeaderCell>` renders a column header within a `<CalendarGridHeader>`. It typically displays a weekday name. Show props | Name | Type | Description | | --- | --- | --- | | `children` | `ReactNode` | The children of the component. | | `className` | `string` | The CSS className for the element. | | `style` | `CSSProperties` | The inline style for the element. | Accessibility | Name | Type | Description | | --- | --- | --- | | `id` | `string` | The element's unique identifier. See MDN. | ### CalendarGridBody# A `<CalendarGridBody>` renders the body within a `<CalendarGrid>`. It accepts a function as its `children`, which is called to render a `<CalendarCell>` for each date. Show props | Name | Type | Description | | --- | --- | --- | | `children` | `( (date: CalendarDate )) => ReactElement` | A function to render a `<CalendarCell>` for a given date. | | `className` | `string` | The CSS className for the element. | | `style` | `CSSProperties` | The inline style for the element. | ### CalendarCell# A `<CalendarCell>` renders an individual date within a `<CalendarGrid>`. Show props | Name | Type | Description | | --- | --- | --- | | `date` | ` CalendarDate ` | The date to render in the cell. | | `children` | `ReactNode | ( (values: CalendarCellRenderProps & & { defaultChildren: ReactNode | | undefined } )) => ReactNode` | The children of the component. A function may be provided to alter the children based on component state. | | `className` | `string | ( (values: CalendarCellRenderProps & & { defaultClassName: string | | undefined } )) => string` | The CSS className for the element. A function may be provided to compute the class based on component state. | | `style` | `CSSProperties | ( (values: CalendarCellRenderProps & & { defaultStyle: CSSProperties } )) => CSSProperties | undefined` | The inline style for the element. A function may be provided to compute the style based on component state. | Events | Name | Type | Description | | --- | --- | --- | | `onHoverStart` | `( (e: HoverEvent )) => void` | Handler that is called when a hover interaction starts. | | `onHoverEnd` | `( (e: HoverEvent )) => void` | Handler that is called when a hover interaction ends. | | `onHoverChange` | `( (isHovering: boolean )) => void` | Handler that is called when the hover state changes. | ## Styling# * * * React Aria components can be styled in many ways, including using CSS classes, inline styles, utility classes (e.g. Tailwind), CSS-in-JS (e.g. Styled Components), etc. By default, all components include a builtin `className` attribute which can be targeted using CSS selectors. These follow the `react-aria-ComponentName` naming convention. .react-aria-DatePicker { /* ... */ } .react-aria-DatePicker { /* ... */ } .react-aria-DatePicker { /* ... */ } A custom `className` can also be specified on any component. This overrides the default `className` provided by React Aria with your own. <DateInput className="my-date-input"> {/* ... */} </DateInput> <DateInput className="my-date-input"> {/* ... */} </DateInput> <DateInput className="my-date-input"> {/* ... */} </DateInput> In addition, some components support multiple UI states (e.g. focused, placeholder, readonly, etc.). React Aria components expose states using data attributes, which you can target in CSS selectors. For example: .react-aria-DateSegment[data-placeholder] { /* ... */ } .react-aria-DateSegment[data-readonly] { /* ... */ } .react-aria-DateSegment[data-placeholder] { /* ... */ } .react-aria-DateSegment[data-readonly] { /* ... */ } .react-aria-DateSegment[data-placeholder] { /* ... */ } .react-aria-DateSegment[data-readonly] { /* ... */ } The `className` and `style` props also accept functions which receive states for styling. This lets you dynamically determine the classes or styles to apply, which is useful when using utility CSS libraries like Tailwind. <DateSegment className={({ isPlaceholder }) => isPlaceholder ? 'bg-gray-300' : 'bg-gray-600'} /> <DateSegment className={({ isPlaceholder }) => isPlaceholder ? 'bg-gray-300' : 'bg-gray-600'} /> <DateSegment className={( { isPlaceholder } ) => isPlaceholder ? 'bg-gray-300' : 'bg-gray-600'} /> Render props may also be used as children to alter what elements are rendered based on the current state. For example, you could render the placeholder as a separate element to always reserve space for it. <DateSegment> {({ text, placeholder, isPlaceholder }) => ( <> <span style={{ visibility: isPlaceholder ? 'visible' : 'hidden' }}> {placeholder} </span> {isPlaceholder ? '' : text} </> )} </DateSegment> <DateSegment> {({ text, placeholder, isPlaceholder }) => ( <> <span style={{ visibility: isPlaceholder ? 'visible' : 'hidden' }} > {placeholder} </span> {isPlaceholder ? '' : text} </> )} </DateSegment> <DateSegment> {( { text, placeholder, isPlaceholder } ) => ( <> <span style={{ visibility: isPlaceholder ? 'visible' : 'hidden' }} > {placeholder} </span> {isPlaceholder ? '' : text} </> )} </DateSegment> The states, selectors, and render props for each component used in a `DatePicker` are documented below. ### DatePicker# A `DatePicker` can be targeted with the `.react-aria-DatePicker` CSS selector, or by overriding with a custom `className`. It supports the following states and render props: | Name | CSS Selector | Description | | --- | --- | --- | | `isFocusWithin` | `[data-focus-within]` | Whether an element within the date picker is focused, either via a mouse or keyboard. | | `isFocusVisible` | `[data-focus-visible]` | Whether an element within the date picker is keyboard focused. | | `isDisabled` | `[data-disabled]` | Whether the date picker is disabled. | | `isInvalid` | `[data-invalid]` | Whether the date picker is invalid. | | `isOpen` | `[data-open]` | Whether the date picker's popover is currently open. | | `state` | `—` | State of the date picker. | ### Label# A `Label` can be targeted with the `.react-aria-Label` CSS selector, or by overriding with a custom `className`. ### Group# A `Group` can be targeted with the `.react-aria-Group` selector, or by overriding with a custom `className`. It supports the following states: | Name | CSS Selector | Description | | --- | --- | --- | | `isHovered` | `[data-hovered]` | Whether the group is currently hovered with a mouse. | | `isFocusWithin` | `[data-focus-within]` | Whether an element within the group is focused, either via a mouse or keyboard. | | `isFocusVisible` | `[data-focus-visible]` | Whether an element within the group is keyboard focused. | | `isDisabled` | `[data-disabled]` | Whether the group is disabled. | | `isInvalid` | `[data-invalid]` | Whether the group is invalid. | ### DateInput# A `DateInput` can be targeted with the `.react-aria-DateInput` CSS selector, or by overriding with a custom `className`. It supports the following states: | Name | CSS Selector | Description | | --- | --- | --- | | `isHovered` | `[data-hovered]` | Whether the date input is currently hovered with a mouse. | | `isFocusWithin` | `[data-focus-within]` | Whether an element within the date input is focused, either via a mouse or keyboard. | | `isFocusVisible` | `[data-focus-visible]` | Whether an element within the date input is keyboard focused. | | `isDisabled` | `[data-disabled]` | Whether the date input is disabled. | | `isInvalid` | `[data-invalid]` | Whether the date input is invalid. | ### DateSegment# A `DateSegment` can be targeted with the `.react-aria-DateSegment` CSS selector, or by overriding with a custom `className`. It supports the following states and render props: | Name | CSS Selector | Description | | --- | --- | --- | | `isHovered` | `[data-hovered]` | Whether the segment is currently hovered with a mouse. | | `isFocused` | `[data-focused]` | Whether the segment is focused, either via a mouse or keyboard. | | `isFocusVisible` | `[data-focus-visible]` | Whether the segment is keyboard focused. | | `isPlaceholder` | `[data-placeholder]` | Whether the value is a placeholder. | | `isReadOnly` | `[data-readonly]` | Whether the segment is read only. | | `isDisabled` | `[data-disabled]` | Whether the date field is disabled. | | `isInvalid` | `[data-invalid]` | Whether the date field is in an invalid state. | | `type` | `[data-type="..."]` | The type of segment. Values include `literal`, `year`, `month`, `day`, etc. | | `text` | `—` | The formatted text for the segment. | | `placeholder` | `—` | A placeholder string for the segment. | ### Button# A Button can be targeted with the `.react-aria-Button` CSS selector, or by overriding with a custom `className`. The next and previous buttons within a calendar can be targeted specifically with the `[slot=previous]` and `[slot=next]` selectors. Buttons support the following states: | Name | CSS Selector | Description | | --- | --- | --- | | `isHovered` | `[data-hovered]` | Whether the button is currently hovered with a mouse. | | `isPressed` | `[data-pressed]` | Whether the button is currently in a pressed state. | | `isFocused` | `[data-focused]` | Whether the button is focused, either via a mouse or keyboard. | | `isFocusVisible` | `[data-focus-visible]` | Whether the button is keyboard focused. | | `isDisabled` | `[data-disabled]` | Whether the button is disabled. | | `isPending` | `[data-pending]` | Whether the button is currently in a pending state. | ### FieldError# A `FieldError` can be targeted with the `.react-aria-FieldError` CSS selector, or by overriding with a custom `className`. It supports the following render props: | Name | Description | | --- | --- | | `isInvalid` | Whether the input value is invalid. | | `validationErrors` | The current error messages for the input if it is invalid, otherwise an empty array. | | `validationDetails` | The native validation details for the input. | ### Popover# The Popover component can be targeted with the `.react-aria-Popover` CSS selector, or by overriding with a custom `className`. Note that it renders in a React Portal, so it will not appear as a descendant of the DatePicker in the DOM. It supports the following states and render props: | Name | CSS Selector | Description | | --- | --- | --- | | `trigger` | `[data-trigger="..."]` | The name of the component that triggered the popover, e.g. "DialogTrigger" or "ComboBox". | | `placement` | `[data-placement="left | right | top | bottom"]` | The placement of the popover relative to the trigger. | | `isEntering` | `[data-entering]` | Whether the popover is currently entering. Use this to apply animations. | | `isExiting` | `[data-exiting]` | Whether the popover is currently exiting. Use this to apply animations. | Within a DatePicker, the popover will have the `data-trigger="DatePicker"` attribute, which can be used to define date picker-specific styles. In addition, the `--trigger-width` CSS custom property will be set on the popover, which you can use to make the popover match the width of the input group. .react-aria-Popover[data-trigger=DatePicker] { width: var(--trigger-width); } .react-aria-Popover[data-trigger=DatePicker] { width: var(--trigger-width); } .react-aria-Popover[data-trigger=DatePicker] { width: var(--trigger-width); } ### Dialog# A Dialog can be targeted with the `.react-aria-Dialog` CSS selector, or by overriding with a custom `className`. ### Calendar# A Calendar can be targeted with the `.react-aria-Calendar` CSS selector, or by overriding with a custom `className`. ### CalendarGrid# A `CalendarGrid` can be targeted with the `.react-aria-CalendarGrid` CSS selector, or by overriding with a custom `className`. When a function is provided as children, a default `<CalendarGridHeader>` and `<CalendarGridBody>` are rendered. If you need to customize the styling of the header cells, you can render them yourself. See the Calendar docs for an example. ### CalendarGridHeader# A `CalendarGridHeader` can be targeted with the `.react-aria-CalendarGridHeader` CSS selector, or by overriding with a custom `className`. ### CalendarHeaderCell# A `CalendarHeaderCell` can be targeted with the `.react-aria-CalendarHeaderCell` CSS selector, or by overriding with a custom `className`. ### CalendarGridBody# A `CalendarGridBody` can be targeted with the `.react-aria-CalendarGridBody` CSS selector, or by overriding with a custom `className`. ### CalendarCell# A `CalendarCell` can be targeted with the `.react-aria-CalendarCell` CSS selector, or by overriding with a custom `className`. It supports the following states: | Name | CSS Selector | Description | | --- | --- | --- | | `date` | `—` | The date that the cell represents. | | `formattedDate` | `—` | The day number formatted according to the current locale. | | `isHovered` | `[data-hovered]` | Whether the cell is currently hovered with a mouse. | | `isPressed` | `[data-pressed]` | Whether the cell is currently being pressed. | | `isSelected` | `[data-selected]` | Whether the cell is selected. | | `isSelectionStart` | `[data-selection-start]` | Whether the cell is the first date in a range selection. | | `isSelectionEnd` | `[data-selection-end]` | Whether the cell is the last date in a range selection. | | `isFocused` | `[data-focused]` | Whether the cell is focused. | | `isFocusVisible` | `[data-focus-visible]` | Whether the cell is keyboard focused. | | `isDisabled` | `[data-disabled]` | Whether the cell is disabled, according to the calendar's `minValue`, `maxValue`, and `isDisabled` props. Disabled dates are not focusable, and cannot be selected by the user. They are typically displayed with a dimmed appearance. | | `isOutsideVisibleRange` | `[data-outside-visible-range]` | Whether the cell is outside the visible range of the calendar. For example, dates before the first day of a month in the same week. | | `isOutsideMonth` | `[data-outside-month]` | Whether the cell is outside the current month. | | `isUnavailable` | `[data-unavailable]` | Whether the cell is unavailable, according to the calendar's `isDateUnavailable` prop. Unavailable dates remain focusable, but cannot be selected by the user. They should be displayed with a visual affordance to indicate they are unavailable, such as a different color or a strikethrough. Note that because they are focusable, unavailable dates must meet a 4.5:1 color contrast ratio, as defined by WCAG. | | `isInvalid` | `[data-invalid]` | Whether the cell is part of an invalid selection. | ### Text# The help text elements within a `DatePicker` can be targeted with the `[slot=description]` and `[slot=errorMessage]` CSS selectors, or by adding a custom `className`. ## Advanced customization# * * * ### Composition# If you need to customize one of the components within a `DatePicker`, such as `Label` or `DateSegment`, in many cases you can create a wrapper component. This lets you customize the props passed to the component. function MyDateSegment(props) { return <MyDateSegment {...props} className="my-date-segment" /> } function MyDateSegment(props) { return ( <MyDateSegment {...props} className="my-date-segment" /> ); } function MyDateSegment( props ) { return ( <MyDateSegment {...props} className="my-date-segment" /> ); } ### Contexts# All React Aria Components export a corresponding context that can be used to send props to them from a parent element. This enables you to build your own compositional APIs similar to those found in React Aria Components itself. You can send any prop or ref via context that you could pass to the corresponding component. The local props and ref on the component are merged with the ones passed via context, with the local props taking precedence (following the rules documented in mergeProps). | Component | Context | Props | Ref | | --- | --- | --- | --- | | `DatePicker` | `DatePickerContext` | ` DatePickerProps ` | `HTMLDivElement` | This example shows a `FieldGroup` component that renders a group of date pickers with a title and optional error message. It uses the useId hook to generate a unique id for the error message. All of the child DatePickers are marked invalid and associated with the error message via the `aria-describedby` attribute passed to the `DatePickerContext` provider. import {DatePickerContext} from 'react-aria-components'; import {useId} from 'react-aria'; interface FieldGroupProps { title?: string; children?: React.ReactNode; errorMessage?: string; } function FieldGroup({ title, children, errorMessage }: FieldGroupProps) { let errorId = useId(); return ( <fieldset> <legend>{title}</legend> <DatePickerContext.Provider value={{ isInvalid: !!errorMessage, 'aria-describedby': errorMessage ? errorId : undefined }} > {children} </DatePickerContext.Provider> {errorMessage && ( <small id={errorId} className="invalid">{errorMessage}</small> )} </fieldset> ); } <FieldGroup title="Dates" errorMessage="Tickets must go on sale before event." > <MyDatePicker label="Event date" defaultValue={parseDate('2023-07-12')} /> <MyDatePicker label="Ticket sale date" defaultValue={parseDate('2023-08-03')} /> </FieldGroup> import {DatePickerContext} from 'react-aria-components'; import {useId} from 'react-aria'; interface FieldGroupProps { title?: string; children?: React.ReactNode; errorMessage?: string; } function FieldGroup( { title, children, errorMessage }: FieldGroupProps ) { let errorId = useId(); return ( <fieldset> <legend>{title}</legend> <DatePickerContext.Provider value={{ isInvalid: !!errorMessage, 'aria-describedby': errorMessage ? errorId : undefined }} > {children} </DatePickerContext.Provider> {errorMessage && ( <small id={errorId} className="invalid"> {errorMessage} </small> )} </fieldset> ); } <FieldGroup title="Dates" errorMessage="Tickets must go on sale before event." > <MyDatePicker label="Event date" defaultValue={parseDate('2023-07-12')} /> <MyDatePicker label="Ticket sale date" defaultValue={parseDate('2023-08-03')} /> </FieldGroup> import {DatePickerContext} from 'react-aria-components'; import {useId} from 'react-aria'; interface FieldGroupProps { title?: string; children?: React.ReactNode; errorMessage?: string; } function FieldGroup( { title, children, errorMessage }: FieldGroupProps ) { let errorId = useId(); return ( <fieldset> <legend> {title} </legend> <DatePickerContext.Provider value={{ isInvalid: !!errorMessage, 'aria-describedby': errorMessage ? errorId : undefined }} > {children} </DatePickerContext.Provider> {errorMessage && ( <small id={errorId} className="invalid" > {errorMessage} </small> )} </fieldset> ); } <FieldGroup title="Dates" errorMessage="Tickets must go on sale before event." > <MyDatePicker label="Event date" defaultValue={parseDate( '2023-07-12' )} /> <MyDatePicker label="Ticket sale date" defaultValue={parseDate( '2023-08-03' )} /> </FieldGroup> Dates Event date 7/12/2023 ▼ Ticket sale date 8/3/2023 ▼ Tickets must go on sale before event. Show CSS fieldset { padding: 1.5em; width: fit-content; } .invalid { color: var(--invalid-color); margin-top: 1em; display: block; } fieldset { padding: 1.5em; width: fit-content; } .invalid { color: var(--invalid-color); margin-top: 1em; display: block; } fieldset { padding: 1.5em; width: fit-content; } .invalid { color: var(--invalid-color); margin-top: 1em; display: block; } ### Custom children# DatePicker passes props to its child components, such as the label and popover, via their associated contexts. These contexts are exported so you can also consume them in your own custom components. This enables you to reuse existing components from your app or component library together with React Aria Components. | Component | Context | Props | Ref | | --- | --- | --- | --- | | `Label` | `LabelContext` | ` LabelProps ` | `HTMLLabelElement` | | `Group` | `GroupContext` | ` GroupProps ` | `HTMLDivElement` | | `Button` | `ButtonContext` | ` ButtonProps ` | `HTMLButtonElement` | | `Popover` | `PopoverContext` | ` PopoverProps ` | `HTMLElement` | | `Dialog` | `DialogContext` | ` DialogProps ` | `HTMLElement` | | `Calendar` | `CalendarContext` | ` CalendarProps ` | `HTMLDivElement` | | `Text` | `TextContext` | ` TextProps ` | `HTMLElement` | This example consumes from `LabelContext` in an existing styled label component to make it compatible with React Aria Components. The `useContextProps` hook merges the local props and ref with the ones provided via context by DatePicker. import type {LabelProps} from 'react-aria-components'; import {LabelContext, useContextProps} from 'react-aria-components'; const MyCustomLabel = React.forwardRef( (props: LabelProps, ref: React.ForwardedRef<HTMLLabelElement>) => { // Merge the local props and ref with the ones provided via context. [props, ref] = useContextProps(props, ref, LabelContext); // ... your existing Label component return <label {...props} ref={ref} />; } ); import type {LabelProps} from 'react-aria-components'; import { LabelContext, useContextProps } from 'react-aria-components'; const MyCustomLabel = React.forwardRef( ( props: LabelProps, ref: React.ForwardedRef<HTMLLabelElement> ) => { // Merge the local props and ref with the ones provided via context. [props, ref] = useContextProps( props, ref, LabelContext ); // ... your existing Label component return <label {...props} ref={ref} />; } ); import type {LabelProps} from 'react-aria-components'; import { LabelContext, useContextProps } from 'react-aria-components'; const MyCustomLabel = React.forwardRef( ( props: LabelProps, ref: React.ForwardedRef< HTMLLabelElement > ) => { // Merge the local props and ref with the ones provided via context. [props, ref] = useContextProps( props, ref, LabelContext ); // ... your existing Label component return ( <label {...props} ref={ref} /> ); } ); Now you can use `MyCustomLabel` within a `DatePicker`, in place of the builtin React Aria Components `Label`. <DatePicker> <MyCustomLabel>Name</MyCustomLabel> {/* ... */} </DatePicker> <DatePicker> <MyCustomLabel>Name</MyCustomLabel> {/* ... */} </DatePicker> <DatePicker> <MyCustomLabel> Name </MyCustomLabel> {/* ... */} </DatePicker> ### State# DatePicker provides a `DatePickerState` object to its children via `DatePickerStateContext`. This can be used to access and manipulate the date picker's state. This example shows a `DatePickerClearButton` component that can be placed within a `DatePicker` to allow the user to clear the selected value. import {DatePickerStateContext} from 'react-aria-components'; function DatePickerClearButton() { let state = React.useContext(DatePickerStateContext)!; return ( <Button // Don't inherit default Button behavior from DatePicker. slot={null} className="clear-button" aria-label="Clear" onPress={() => state.setValue(null)}> ✕ </Button> ); } <DatePicker defaultValue={today(getLocalTimeZone())}> <Label>Date</Label> <Group> <DateInput> {segment => <DateSegment segment={segment} />} </DateInput> <DatePickerClearButton /> <Button>▼</Button> </Group> <Popover> <Dialog> <Calendar> <header> <Button slot="previous">◀</Button> <Heading /> <Button slot="next">▶</Button> </header> <CalendarGrid> {date => <CalendarCell date={date} />} </CalendarGrid> </Calendar> </Dialog> </Popover> </DatePicker> import {DatePickerStateContext} from 'react-aria-components'; function DatePickerClearButton() { let state = React.useContext(DatePickerStateContext)!; return ( <Button // Don't inherit default Button behavior from DatePicker. slot={null} className="clear-button" aria-label="Clear" onPress={() => state.setValue(null)} > ✕ </Button> ); } <DatePicker defaultValue={today(getLocalTimeZone())}> <Label>Date</Label> <Group> <DateInput> {(segment) => <DateSegment segment={segment} />} </DateInput> <DatePickerClearButton /> <Button>▼</Button> </Group> <Popover> <Dialog> <Calendar> <header> <Button slot="previous">◀</Button> <Heading /> <Button slot="next">▶</Button> </header> <CalendarGrid> {(date) => <CalendarCell date={date} />} </CalendarGrid> </Calendar> </Dialog> </Popover> </DatePicker> import {DatePickerStateContext} from 'react-aria-components'; function DatePickerClearButton() { let state = React .useContext( DatePickerStateContext )!; return ( <Button // Don't inherit default Button behavior from DatePicker. slot={null} className="clear-button" aria-label="Clear" onPress={() => state.setValue( null )} > ✕ </Button> ); } <DatePicker defaultValue={today( getLocalTimeZone() )} > <Label>Date</Label> <Group> <DateInput> {(segment) => ( <DateSegment segment={segment} /> )} </DateInput> <DatePickerClearButton /> <Button>▼</Button> </Group> <Popover> <Dialog> <Calendar> <header> <Button slot="previous"> ◀ </Button> <Heading /> <Button slot="next"> ▶ </Button> </header> <CalendarGrid> {(date) => ( <CalendarCell date={date} /> )} </CalendarGrid> </Calendar> </Dialog> </Popover> </DatePicker> Date 6/18/2025 ✕▼ Show CSS .clear-button { width: 1.143rem; height: 1.143rem; border-radius: 1.143rem; margin-left: -3.4rem; font-size: 0.857rem; line-height: 0.857rem; vertical-align: middle; text-align: center; background: gray; color: white; border: none; padding: 0; outline: none; &[data-pressed] { background: dimgray; } &[data-focus-visible] { outline: 2px solid var(--focus-ring-color); outline-offset: 2px; } + .react-aria-Button { margin-left: 4px; } } .clear-button { width: 1.143rem; height: 1.143rem; border-radius: 1.143rem; margin-left: -3.4rem; font-size: 0.857rem; line-height: 0.857rem; vertical-align: middle; text-align: center; background: gray; color: white; border: none; padding: 0; outline: none; &[data-pressed] { background: dimgray; } &[data-focus-visible] { outline: 2px solid var(--focus-ring-color); outline-offset: 2px; } + .react-aria-Button { margin-left: 4px; } } .clear-button { width: 1.143rem; height: 1.143rem; border-radius: 1.143rem; margin-left: -3.4rem; font-size: 0.857rem; line-height: 0.857rem; vertical-align: middle; text-align: center; background: gray; color: white; border: none; padding: 0; outline: none; &[data-pressed] { background: dimgray; } &[data-focus-visible] { outline: 2px solid var(--focus-ring-color); outline-offset: 2px; } + .react-aria-Button { margin-left: 4px; } } ### Hooks# If you need to customize things even further, such as accessing internal state, intercepting events, or customizing the DOM structure, you can drop down to the lower level Hook-based API. See useDatePicker for more details. --- ## Page: https://react-spectrum.adobe.com/react-aria/DateRangePicker.html # DateRangePicker A date range picker combines two DateFields and a RangeCalendar popover to allow users to enter or select a date and time range. <table><tbody><tr><th>install</th><td><code>yarn add react-aria-components</code></td></tr><tr><th>version</th><td>1.10.1</td></tr><tr><th>usage</th><td><code><span>import</span> {DateRangePicker} <span>from</span> <span>'react-aria-components'</span></code></td></tr></tbody></table> View repository GitHub View package NPM ## Example# * * * import {Button, CalendarCell, CalendarGrid, DateInput, DateRangePicker, DateSegment, Dialog, Group, Heading, Label, Popover, RangeCalendar} from 'react-aria-components'; <DateRangePicker> <Label>Trip dates</Label> <Group> <DateInput slot="start"> {(segment) => <DateSegment segment={segment} />} </DateInput> <span aria-hidden="true">–</span> <DateInput slot="end"> {(segment) => <DateSegment segment={segment} />} </DateInput> <Button>▼</Button> </Group> <Popover> <Dialog> <RangeCalendar> <header> <Button slot="previous">◀</Button> <Heading /> <Button slot="next">▶</Button> </header> <CalendarGrid> {(date) => <CalendarCell date={date} />} </CalendarGrid> </RangeCalendar> </Dialog> </Popover> </DateRangePicker> import { Button, CalendarCell, CalendarGrid, DateInput, DateRangePicker, DateSegment, Dialog, Group, Heading, Label, Popover, RangeCalendar } from 'react-aria-components'; <DateRangePicker> <Label>Trip dates</Label> <Group> <DateInput slot="start"> {(segment) => <DateSegment segment={segment} />} </DateInput> <span aria-hidden="true">–</span> <DateInput slot="end"> {(segment) => <DateSegment segment={segment} />} </DateInput> <Button>▼</Button> </Group> <Popover> <Dialog> <RangeCalendar> <header> <Button slot="previous">◀</Button> <Heading /> <Button slot="next">▶</Button> </header> <CalendarGrid> {(date) => <CalendarCell date={date} />} </CalendarGrid> </RangeCalendar> </Dialog> </Popover> </DateRangePicker> import { Button, CalendarCell, CalendarGrid, DateInput, DateRangePicker, DateSegment, Dialog, Group, Heading, Label, Popover, RangeCalendar } from 'react-aria-components'; <DateRangePicker> <Label> Trip dates </Label> <Group> <DateInput slot="start"> {(segment) => ( <DateSegment segment={segment} /> )} </DateInput> <span aria-hidden="true"> – </span> <DateInput slot="end"> {(segment) => ( <DateSegment segment={segment} /> )} </DateInput> <Button>▼</Button> </Group> <Popover> <Dialog> <RangeCalendar> <header> <Button slot="previous"> ◀ </Button> <Heading /> <Button slot="next"> ▶ </Button> </header> <CalendarGrid> {(date) => ( <CalendarCell date={date} /> )} </CalendarGrid> </RangeCalendar> </Dialog> </Popover> </DateRangePicker> Trip dates mm/dd/yyyy – mm/dd/yyyy ▼ Show CSS @import "@react-aria/example-theme"; .react-aria-DateRangePicker { color: var(--text-color); .react-aria-Group { display: flex; align-items: center; width: fit-content; min-width: 220px; max-width: 100%; box-sizing: border-box; overflow: auto; position: relative; padding: 4px 4px 4px 8px; border: 1px solid var(--border-color); border-radius: 6px; background: var(--field-background); white-space: nowrap; &[data-pressed] { box-shadow: none; background: var(--highlight-background); } &[data-focus-within] { outline: 2px solid var(--focus-ring-color); outline-offset: -1px; } } [slot=start] + span { padding: 0 4px; } [slot=end] { margin-right: 2rem; flex: 1; } .react-aria-Button { background: var(--highlight-background); color: var(--highlight-foreground); border: 2px solid var(--field-background); forced-color-adjust: none; border-radius: 4px; border: none; margin-left: auto; width: 1.429rem; height: 1.429rem; padding: 0; font-size: 0.857rem; box-sizing: content-box; flex-shrink: 0; position: sticky; right: 0; &[data-focus-visible] { outline: 2px solid var(--focus-ring-color); outline-offset: 2px; } } .react-aria-DateInput { width: unset; min-width: unset; padding: unset; border: unset; outline: unset; } } .react-aria-Popover[data-trigger=DateRangePicker] { max-width: unset; } @import "@react-aria/example-theme"; .react-aria-DateRangePicker { color: var(--text-color); .react-aria-Group { display: flex; align-items: center; width: fit-content; min-width: 220px; max-width: 100%; box-sizing: border-box; overflow: auto; position: relative; padding: 4px 4px 4px 8px; border: 1px solid var(--border-color); border-radius: 6px; background: var(--field-background); white-space: nowrap; &[data-pressed] { box-shadow: none; background: var(--highlight-background); } &[data-focus-within] { outline: 2px solid var(--focus-ring-color); outline-offset: -1px; } } [slot=start] + span { padding: 0 4px; } [slot=end] { margin-right: 2rem; flex: 1; } .react-aria-Button { background: var(--highlight-background); color: var(--highlight-foreground); border: 2px solid var(--field-background); forced-color-adjust: none; border-radius: 4px; border: none; margin-left: auto; width: 1.429rem; height: 1.429rem; padding: 0; font-size: 0.857rem; box-sizing: content-box; flex-shrink: 0; position: sticky; right: 0; &[data-focus-visible] { outline: 2px solid var(--focus-ring-color); outline-offset: 2px; } } .react-aria-DateInput { width: unset; min-width: unset; padding: unset; border: unset; outline: unset; } } .react-aria-Popover[data-trigger=DateRangePicker] { max-width: unset; } @import "@react-aria/example-theme"; .react-aria-DateRangePicker { color: var(--text-color); .react-aria-Group { display: flex; align-items: center; width: fit-content; min-width: 220px; max-width: 100%; box-sizing: border-box; overflow: auto; position: relative; padding: 4px 4px 4px 8px; border: 1px solid var(--border-color); border-radius: 6px; background: var(--field-background); white-space: nowrap; &[data-pressed] { box-shadow: none; background: var(--highlight-background); } &[data-focus-within] { outline: 2px solid var(--focus-ring-color); outline-offset: -1px; } } [slot=start] + span { padding: 0 4px; } [slot=end] { margin-right: 2rem; flex: 1; } .react-aria-Button { background: var(--highlight-background); color: var(--highlight-foreground); border: 2px solid var(--field-background); forced-color-adjust: none; border-radius: 4px; border: none; margin-left: auto; width: 1.429rem; height: 1.429rem; padding: 0; font-size: 0.857rem; box-sizing: content-box; flex-shrink: 0; position: sticky; right: 0; &[data-focus-visible] { outline: 2px solid var(--focus-ring-color); outline-offset: 2px; } } .react-aria-DateInput { width: unset; min-width: unset; padding: unset; border: unset; outline: unset; } } .react-aria-Popover[data-trigger=DateRangePicker] { max-width: unset; } ## Features# * * * A date range picker can be built using two separate `<input type="date">` elements, but this is very limited in functionality, lacking in internationalization capabilities, inconsistent between browsers, and difficult to style. `DateRangePicker` helps achieve accessible and international date and time range pickers that can be styled as needed. * **Dates and times** – Support for dates and times with configurable granularity. * **International** – Support for 13 calendar systems used around the world, including Gregorian, Buddhist, Islamic, Persian, and more. Locale-specific formatting, number systems, hour cycles, and right-to-left support are available as well. * **Time zone aware** – Dates and times can optionally include a time zone. All modifications follow time zone rules such as daylight saving time. * **Accessible** – Each date and time unit is displayed as an individually focusable and editable segment, which allows users an easy way to edit dates using the keyboard, in any date format and locale. Users can also open a calendar popover to select date ranges in a standard month grid. Localized screen reader messages are included to announce when the selection and visible date range change. * **Touch friendly** – Date segments are editable using an easy to use numeric keypad, date ranges can be selected by dragging over dates in the calendar using a touch screen, and all interactions are accessible using touch-based screen readers. * **Validation** – Integrates with HTML forms, supporting required, minimum and maximum values, unavailable dates, custom validation functions, realtime validation, and server-side validation errors. * **Customizable** – As with all of React Aria, the DOM structure and styling of all elements can be fully customized. Read our blog post for more details about the internationalization, accessibility, and user experience features implemented by `DateRangePicker`. ## Anatomy# * * * A date range picker consists of a label, and group containing two date fields and a button. Clicking the button opens a popup containing a range calendar. The date fields include segments representing each unit of a date and time (e.g. years, months, days, etc.), each of which is individually focusable and editable using the keyboard. The calendar popup offers a more visual way of choosing a date range. `DateRangePicker` also supports optional description and error message elements, which can be used to provide more context about the field, and any validation messages. These are linked with the input via the `aria-describedby` attribute. import {Button, CalendarCell, CalendarGrid, CalendarGridBody, CalendarGridHeader, CalendarHeaderCell, DateInput, DateRangePicker, DateSegment, Dialog, FieldError, Group, Heading, Label, Popover, RangeCalendar, Text} from 'react-aria-components'; <DateRangePicker> <Label /> <Group> <DateInput slot="start"> {(segment) => <DateSegment segment={segment} />} </DateInput> <DateInput slot="end"> {(segment) => <DateSegment segment={segment} />} </DateInput> <Button /> </Group> <Text slot="description" /> <FieldError /> <Popover> <Dialog> <RangeCalendar> <Button slot="previous" /> <Heading /> <Button slot="next" /> <CalendarGrid> <CalendarGridHeader> {(day) => <CalendarHeaderCell />} </CalendarGridHeader> <CalendarGridBody> {(date) => <CalendarCell date={date} />} </CalendarGridBody> </CalendarGrid> <Text slot="errorMessage" /> </RangeCalendar> </Dialog> </Popover> </DateRangePicker> import { Button, CalendarCell, CalendarGrid, CalendarGridBody, CalendarGridHeader, CalendarHeaderCell, DateInput, DateRangePicker, DateSegment, Dialog, FieldError, Group, Heading, Label, Popover, RangeCalendar, Text } from 'react-aria-components'; <DateRangePicker> <Label /> <Group> <DateInput slot="start"> {(segment) => <DateSegment segment={segment} />} </DateInput> <DateInput slot="end"> {(segment) => <DateSegment segment={segment} />} </DateInput> <Button /> </Group> <Text slot="description" /> <FieldError /> <Popover> <Dialog> <RangeCalendar> <Button slot="previous" /> <Heading /> <Button slot="next" /> <CalendarGrid> <CalendarGridHeader> {(day) => <CalendarHeaderCell />} </CalendarGridHeader> <CalendarGridBody> {(date) => <CalendarCell date={date} />} </CalendarGridBody> </CalendarGrid> <Text slot="errorMessage" /> </RangeCalendar> </Dialog> </Popover> </DateRangePicker> import { Button, CalendarCell, CalendarGrid, CalendarGridBody, CalendarGridHeader, CalendarHeaderCell, DateInput, DateRangePicker, DateSegment, Dialog, FieldError, Group, Heading, Label, Popover, RangeCalendar, Text } from 'react-aria-components'; <DateRangePicker> <Label /> <Group> <DateInput slot="start"> {(segment) => ( <DateSegment segment={segment} /> )} </DateInput> <DateInput slot="end"> {(segment) => ( <DateSegment segment={segment} /> )} </DateInput> <Button /> </Group> <Text slot="description" /> <FieldError /> <Popover> <Dialog> <RangeCalendar> <Button slot="previous" /> <Heading /> <Button slot="next" /> <CalendarGrid> <CalendarGridHeader> {(day) => ( <CalendarHeaderCell /> )} </CalendarGridHeader> <CalendarGridBody> {(date) => ( <CalendarCell date={date} /> )} </CalendarGridBody> </CalendarGrid> <Text slot="errorMessage" /> </RangeCalendar> </Dialog> </Popover> </DateRangePicker> If the date range picker does not have a visible label, an `aria-label` or `aria-labelledby` prop must be passed instead to identify it to assistive technology. Note that most of this anatomy is shared with DatePicker, so you can reuse many components between them if you have both. ### Internationalization# To ensure the proper date and time format in RTL locales, `DateInput` must have `display` set to either `inline`, `inline-block`, or `block`. ### Concepts# `DateRangePicker` makes use of the following concepts: @internationalized/date Represent and manipulate dates and times in a locale-aware manner. Forms Validating and submitting form data, and integrating with form libraries. ### Composed components# A `DateRangePicker` uses the following components, which may also be used standalone or reused in other components. Label A label provides context for an input element. DateField A date field allows a user to enter and edit date values using a keyboard. Button A button allows a user to perform an action. Popover A popover displays content in context with a trigger element. Dialog A dialog is an overlay shown above other content in an application. RangeCalendar A range calendar allows a user to select a contiguous range of dates. ## Starter kits# * * * To help kick-start your project, we offer starter kits that include example implementations of all React Aria components with various styling solutions. All components are fully styled, including support for dark mode, high contrast mode, and all UI states. Each starter comes with a pre-configured Storybook that you can experiment with, or use as a starting point for your own component library. Vanilla CSS Download ZIP Preview Tailwind CSS Download ZIP Preview ## Reusable wrappers# * * * If you will use a DateRangePicker in multiple places in your app, you can wrap all of the pieces into a reusable component. This way, the DOM structure, styling code, and other logic are defined in a single place and reused everywhere to ensure consistency. This example wraps `DateRangePicker` and all of its children together into a single component which accepts a `label` prop, which is passed to the right place. It also shows how to use the `description` slot to render help text, and `FieldError` component to render validation errors. import type {DateRangePickerProps, DateValue, ValidationResult} from 'react-aria-components'; import {FieldError, Text} from 'react-aria-components'; interface MyDateRangePickerProps<T extends DateValue> extends DateRangePickerProps<T> { label?: string; description?: string; errorMessage?: string | ((validation: ValidationResult) => string); } function MyDateRangePicker<T extends DateValue>( { label, description, errorMessage, firstDayOfWeek, ...props }: MyDateRangePickerProps<T> ) { return ( <DateRangePicker {...props}> <Label>{label}</Label> <Group> <DateInput slot="start"> {(segment) => <DateSegment segment={segment} />} </DateInput> <span aria-hidden="true">–</span> <DateInput slot="end"> {(segment) => <DateSegment segment={segment} />} </DateInput> <Button>▼</Button> </Group> {description && <Text slot="description">{description}</Text>} <FieldError>{errorMessage}</FieldError> <Popover> <Dialog> <RangeCalendar firstDayOfWeek={firstDayOfWeek}> <header> <Button slot="previous">◀</Button> <Heading /> <Button slot="next">▶</Button> </header> <CalendarGrid> {(date) => <CalendarCell date={date} />} </CalendarGrid> </RangeCalendar> </Dialog> </Popover> </DateRangePicker> ); } <MyDateRangePicker label="Event date" /> import type { DateRangePickerProps, DateValue, ValidationResult } from 'react-aria-components'; import {FieldError, Text} from 'react-aria-components'; interface MyDateRangePickerProps<T extends DateValue> extends DateRangePickerProps<T> { label?: string; description?: string; errorMessage?: | string | ((validation: ValidationResult) => string); } function MyDateRangePicker<T extends DateValue>( { label, description, errorMessage, firstDayOfWeek, ...props }: MyDateRangePickerProps<T> ) { return ( <DateRangePicker {...props}> <Label>{label}</Label> <Group> <DateInput slot="start"> {(segment) => <DateSegment segment={segment} />} </DateInput> <span aria-hidden="true">–</span> <DateInput slot="end"> {(segment) => <DateSegment segment={segment} />} </DateInput> <Button>▼</Button> </Group> {description && ( <Text slot="description">{description}</Text> )} <FieldError>{errorMessage}</FieldError> <Popover> <Dialog> <RangeCalendar firstDayOfWeek={firstDayOfWeek}> <header> <Button slot="previous">◀</Button> <Heading /> <Button slot="next">▶</Button> </header> <CalendarGrid> {(date) => <CalendarCell date={date} />} </CalendarGrid> </RangeCalendar> </Dialog> </Popover> </DateRangePicker> ); } <MyDateRangePicker label="Event date" /> import type { DateRangePickerProps, DateValue, ValidationResult } from 'react-aria-components'; import { FieldError, Text } from 'react-aria-components'; interface MyDateRangePickerProps< T extends DateValue > extends DateRangePickerProps< T > { label?: string; description?: string; errorMessage?: | string | (( validation: ValidationResult ) => string); } function MyDateRangePicker< T extends DateValue >({ label, description, errorMessage, firstDayOfWeek, ...props }: MyDateRangePickerProps< T >) { return ( <DateRangePicker {...props} > <Label> {label} </Label> <Group> <DateInput slot="start"> {(segment) => ( <DateSegment segment={segment} /> )} </DateInput> <span aria-hidden="true"> – </span> <DateInput slot="end"> {(segment) => ( <DateSegment segment={segment} /> )} </DateInput> <Button> ▼ </Button> </Group> {description && ( <Text slot="description"> {description} </Text> )} <FieldError> {errorMessage} </FieldError> <Popover> <Dialog> <RangeCalendar firstDayOfWeek={firstDayOfWeek} > <header> <Button slot="previous"> ◀ </Button> <Heading /> <Button slot="next"> ▶ </Button> </header> <CalendarGrid> {(date) => ( <CalendarCell date={date} /> )} </CalendarGrid> </RangeCalendar> </Dialog> </Popover> </DateRangePicker> ); } <MyDateRangePicker label="Event date" /> Event date mm/dd/yyyy – mm/dd/yyyy ▼ ## Value# * * * A `DateRangePicker` displays a placeholder by default. An initial, uncontrolled value can be provided to the `DateRangePicker` using the `defaultValue` prop. Alternatively, a controlled value can be provided using the `value` prop. Date ranges are objects with `start` and `end` properties containing date values, which are provided using objects in the @internationalized/date package. This library handles correct international date manipulation across calendars, time zones, and other localization concerns. `DateRangePicker` supports values of the following types: * ` CalendarDate `– a date without any time components. May be parsed from a string representation using the` parseDate `function. Use this type to represent dates where the time is not important, such as a birthday or an all day calendar event. * ` CalendarDateTime `– a date with a time, but not in any specific time zone. May be parsed from a string representation using the` parseDateTime `function. Use this type to represent times that occur at the same local time regardless of the time zone, such as the time of New Years Eve fireworks which always occur at midnight. Most times are better stored as a `ZonedDateTime`. * ` ZonedDateTime `– a date with a time in a specific time zone. May be parsed from a string representation using the` parseZonedDateTime `,` parseAbsolute `, or` parseAbsoluteToLocal `functions. Use this type to represent an exact moment in time at a particular location on Earth. import {parseDate} from '@internationalized/date'; function Example() { let [value, setValue] = React.useState({ start: parseDate('2020-02-03'), end: parseDate('2020-02-08') }); return ( <> <MyDateRangePicker label="Date range (uncontrolled)" defaultValue={{ start: parseDate('2020-02-03'), end: parseDate('2020-02-08') }} /> <MyDateRangePicker label="Date range (controlled)" value={value} onChange={setValue} /> </> ); } import {parseDate} from '@internationalized/date'; function Example() { let [value, setValue] = React.useState({ start: parseDate('2020-02-03'), end: parseDate('2020-02-08') }); return ( <> <MyDateRangePicker label="Date range (uncontrolled)" defaultValue={{ start: parseDate('2020-02-03'), end: parseDate('2020-02-08') }} /> <MyDateRangePicker label="Date range (controlled)" value={value} onChange={setValue} /> </> ); } import {parseDate} from '@internationalized/date'; function Example() { let [value, setValue] = React.useState({ start: parseDate( '2020-02-03' ), end: parseDate( '2020-02-08' ) }); return ( <> <MyDateRangePicker label="Date range (uncontrolled)" defaultValue={{ start: parseDate( '2020-02-03' ), end: parseDate( '2020-02-08' ) }} /> <MyDateRangePicker label="Date range (controlled)" value={value} onChange={setValue} /> </> ); } Date range (uncontrolled) 2/3/2020 – 2/8/2020 ▼ Date range (controlled) 2/3/2020 – 2/8/2020 ▼ ### Time zones# `DateRangePicker` is time zone aware when `ZonedDateTime` objects are provided as the value. In this case, the time zone abbreviation is displayed, and time zone concerns such as daylight saving time are taken into account when the value is manipulated. In most cases, your data will come from and be sent to a server as an ISO 8601 formatted string. @internationalized/date includes functions for parsing strings in multiple formats into `ZonedDateTime` objects. Which format you use will depend on what information you need to store. * ` parseZonedDateTime `– This function parses a date with an explicit time zone and optional UTC offset attached (e.g. `"2021-11-07T00:45[America/Los_Angeles]"` or `"2021-11-07T00:45-07:00[America/Los_Angeles]"`). This format preserves the maximum amount of information. If the exact local time and time zone that a user selected is important, use this format. Storing the time zone and offset that was selected rather than converting to UTC ensures that the local time is correct regardless of daylight saving rule changes (e.g. if a locale abolishes DST). Examples where this applies include calendar events, reminders, and other times that occur in a particular location. * ` parseAbsolute `– This function parses an absolute date and time that occurs at the same instant at all locations on Earth. It can be represented in UTC (e.g. `"2021-11-07T07:45:00Z"`), or stored with a particular offset (e.g. `"2021-11-07T07:45:00-07:00"`). A time zone identifier, e.g. `America/Los_Angeles`, must be passed, and the result will be converted into that time zone. Absolute times are the best way to represent events that occurred in the past, or future events where an exact time is needed, regardless of time zone. * ` parseAbsoluteToLocal `– This function parses an absolute date and time into the current user's local time zone. It is a shortcut for `parseAbsolute`, and accepts the same formats. import {parseZonedDateTime} from '@internationalized/date'; <MyDateRangePicker label="Date range" defaultValue={{ start: parseZonedDateTime('2022-11-07T00:45[America/Los_Angeles]'), end: parseZonedDateTime('2022-11-08T11:15[America/Los_Angeles]') }} /> import {parseZonedDateTime} from '@internationalized/date'; <MyDateRangePicker label="Date range" defaultValue={{ start: parseZonedDateTime( '2022-11-07T00:45[America/Los_Angeles]' ), end: parseZonedDateTime( '2022-11-08T11:15[America/Los_Angeles]' ) }} /> import {parseZonedDateTime} from '@internationalized/date'; <MyDateRangePicker label="Date range" defaultValue={{ start: parseZonedDateTime( '2022-11-07T00:45[America/Los_Angeles]' ), end: parseZonedDateTime( '2022-11-08T11:15[America/Los_Angeles]' ) }} /> Date range 11/7/2022, 12:45 AM PST – 11/8/2022, 11:15 AM PST ▼ `DateRangePicker` displays times in the time zone included in the `ZonedDateTime` object. The above example is always displayed in Pacific Standard Time because the `America/Los_Angeles` time zone identifier is provided. @internationalized/date includes functions for converting dates between time zones, or parsing a date directly into a specific time zone or the user's local time zone, as shown below. import {parseAbsoluteToLocal} from '@internationalized/date'; <MyDateRangePicker label="Date range" defaultValue={{ start: parseAbsoluteToLocal('2021-11-07T07:45:00Z'), end: parseAbsoluteToLocal('2021-11-08T14:25:00Z') }} /> import {parseAbsoluteToLocal} from '@internationalized/date'; <MyDateRangePicker label="Date range" defaultValue={{ start: parseAbsoluteToLocal('2021-11-07T07:45:00Z'), end: parseAbsoluteToLocal('2021-11-08T14:25:00Z') }} /> import {parseAbsoluteToLocal} from '@internationalized/date'; <MyDateRangePicker label="Date range" defaultValue={{ start: parseAbsoluteToLocal( '2021-11-07T07:45:00Z' ), end: parseAbsoluteToLocal( '2021-11-08T14:25:00Z' ) }} /> Date range 11/7/2021, 7:45 AM UTC – 11/8/2021, 2:25 PM UTC ▼ ### Granularity# The `granularity` prop allows you to control the smallest unit that is displayed by `DateRangePicker`. By default, `CalendarDate` values are displayed with `"day"` granularity (year, month, and day), and `CalendarDateTime` and `ZonedDateTime` values are displayed with `"minute"` granularity. More granular time values can be displayed by setting the `granularity` prop to `"second"`. In addition, when a value with a time is provided but you wish to only display the date, you can set the granularity to `"day"`. This has no effect on the actual value (it still has a time component), only on what fields are displayed. In the following example, two DateRangePickers are synchronized with the same value, but display different granularities. function Example() { let [date, setDate] = React.useState({ start: parseAbsoluteToLocal('2021-04-07T18:45:22Z'), end: parseAbsoluteToLocal('2021-04-08T20:00:00Z') }); return ( <> <MyDateRangePicker label="Date and time range" granularity="second" value={date} onChange={setDate} /> <MyDateRangePicker label="Date range" granularity="day" value={date} onChange={setDate} /> </> ); } function Example() { let [date, setDate] = React.useState({ start: parseAbsoluteToLocal('2021-04-07T18:45:22Z'), end: parseAbsoluteToLocal('2021-04-08T20:00:00Z') }); return ( <> <MyDateRangePicker label="Date and time range" granularity="second" value={date} onChange={setDate} /> <MyDateRangePicker label="Date range" granularity="day" value={date} onChange={setDate} /> </> ); } function Example() { let [date, setDate] = React.useState({ start: parseAbsoluteToLocal( '2021-04-07T18:45:22Z' ), end: parseAbsoluteToLocal( '2021-04-08T20:00:00Z' ) }); return ( <> <MyDateRangePicker label="Date and time range" granularity="second" value={date} onChange={setDate} /> <MyDateRangePicker label="Date range" granularity="day" value={date} onChange={setDate} /> </> ); } Date and time range 4/7/2021, 6:45:22 PM UTC – 4/8/2021, 8:00:00 PM UTC ▼ Date range 4/7/2021 – 4/8/2021 ▼ If no `value` or `defaultValue` prop is passed, then the `granularity` prop also affects which type of value is emitted from the `onChange` event. Note that by default, time values will not have a time zone because none was supplied. You can override this by setting the `placeholderValue` prop explicitly. Values emitted from `onChange` will use the time zone of the placeholder value. import {now} from '@internationalized/date'; <MyDateRangePicker label="Date range" granularity="second" /> <MyDateRangePicker label="Date range" placeholderValue={now('America/New_York')} granularity="second" /> import {now} from '@internationalized/date'; <MyDateRangePicker label="Date range" granularity="second" /> <MyDateRangePicker label="Date range" placeholderValue={now('America/New_York')} granularity="second" /> import {now} from '@internationalized/date'; <MyDateRangePicker label="Date range" granularity="second" /> <MyDateRangePicker label="Date range" placeholderValue={now( 'America/New_York' )} granularity="second" /> Date range mm/dd/yyyy, ––:––:–– AM – mm/dd/yyyy, ––:––:–– AM ▼ Date range mm/dd/yyyy, ––:––:–– AM EDT – mm/dd/yyyy, ––:––:–– AM EDT ▼ ### International calendars# `DateRangePicker` supports selecting dates in many calendar systems used around the world, including Gregorian, Hebrew, Indian, Islamic, Buddhist, and more. Dates are automatically displayed in the appropriate calendar system for the user's locale. The calendar system can be overridden using the Unicode calendar locale extension, passed to the I18nProvider component. Selected dates passed to `onChange` always use the same calendar system as the `value` or `defaultValue` prop. If no `value` or `defaultValue` is provided, then dates passed to `onChange` are always in the Gregorian calendar since this is the most commonly used. This means that even though the user selects dates in their local calendar system, applications are able to deal with dates from all users consistently. The below example displays a `DateRangePicker` in the Hindi language, using the Indian calendar. Dates emitted from `onChange` are in the Gregorian calendar. import {I18nProvider} from 'react-aria'; import type {DateRange} from 'react-aria-components'; function Example() { let [range, setRange] = React.useState<DateRange | null>(null); return ( <I18nProvider locale="hi-IN-u-ca-indian"> <MyDateRangePicker label="Date range" value={range} onChange={setRange} /> <p>Start date: {range?.start.toString()}</p> <p>End date: {range?.end.toString()}</p> </I18nProvider> ); } import {I18nProvider} from 'react-aria'; import type {DateRange} from 'react-aria-components'; function Example() { let [range, setRange] = React.useState<DateRange | null>( null ); return ( <I18nProvider locale="hi-IN-u-ca-indian"> <MyDateRangePicker label="Date range" value={range} onChange={setRange} /> <p>Start date: {range?.start.toString()}</p> <p>End date: {range?.end.toString()}</p> </I18nProvider> ); } import {I18nProvider} from 'react-aria'; import type {DateRange} from 'react-aria-components'; function Example() { let [range, setRange] = React.useState< DateRange | null >(null); return ( <I18nProvider locale="hi-IN-u-ca-indian"> <MyDateRangePicker label="Date range" value={range} onChange={setRange} /> <p> Start date:{' '} {range?.start .toString()} </p> <p> End date:{' '} {range?.end .toString()} </p> </I18nProvider> ); } Date range dd/mm/yyyy शक – dd/mm/yyyy शक ▼ Start date: End date: ### HTML forms# DateRangePicker supports the `startName` and `endName` props for integration with HTML forms. The values will be submitted to the server as ISO 8601 formatted strings according to the granularity of the value. For example, if the date range picker allows selecting only dates then strings such as `"2023-02-03"` will be submitted, and if it allows selecting times then strings such as `"2023-02-03T08:45:00"` will be submitted. See the Value section above for more details about the supported value types. <MyDateRangePicker label="Trip dates" startName="startDate" endName="endDate" /> <MyDateRangePicker label="Trip dates" startName="startDate" endName="endDate" /> <MyDateRangePicker label="Trip dates" startName="startDate" endName="endDate" /> Trip dates mm/dd/yyyy – mm/dd/yyyy ▼ ## Events# * * * `DateRangePicker` accepts an `onChange` prop which is triggered whenever the start or end date is edited by the user. The example below uses `onChange` to update a separate element with a formatted version of the date range in the user's locale and local time zone. This is done by converting the dates to native JavaScript `Date` objects to pass to the formatter. import {useDateFormatter} from 'react-aria'; import {getLocalTimeZone} from '@internationalized/date'; function Example() { let [range, setRange] = React.useState({ start: parseDate('2020-07-03'), end: parseDate('2020-07-10') }); let formatter = useDateFormatter({ dateStyle: 'long' }); return ( <> <MyDateRangePicker label="Date range" value={range} onChange={setRange} /> <p> Selected date: {range ? formatter.formatRange( range.start.toDate(getLocalTimeZone()), range.end.toDate(getLocalTimeZone()) ) : '--'} </p> </> ); } import {useDateFormatter} from 'react-aria'; import {getLocalTimeZone} from '@internationalized/date'; function Example() { let [range, setRange] = React.useState({ start: parseDate('2020-07-03'), end: parseDate('2020-07-10') }); let formatter = useDateFormatter({ dateStyle: 'long' }); return ( <> <MyDateRangePicker label="Date range" value={range} onChange={setRange} /> <p> Selected date: {range ? formatter.formatRange( range.start.toDate(getLocalTimeZone()), range.end.toDate(getLocalTimeZone()) ) : '--'} </p> </> ); } import {useDateFormatter} from 'react-aria'; import {getLocalTimeZone} from '@internationalized/date'; function Example() { let [range, setRange] = React.useState({ start: parseDate( '2020-07-03' ), end: parseDate( '2020-07-10' ) }); let formatter = useDateFormatter({ dateStyle: 'long' }); return ( <> <MyDateRangePicker label="Date range" value={range} onChange={setRange} /> <p> Selected date: {' '} {range ? formatter .formatRange( range.start .toDate( getLocalTimeZone() ), range.end .toDate( getLocalTimeZone() ) ) : '--'} </p> </> ); } Date range 7/3/2020 – 7/10/2020 ▼ Selected date: July 3 – 10, 2020 ## Validation# * * * DateRangePicker supports the `isRequired` prop to ensure the user enters a value, as well as minimum and maximum values, and custom client and server-side validation. It can also be integrated with other form libraries. See the Forms guide to learn more. To display validation errors, add a `<FieldError>` element as a child of the DateRangePicker. This allows you to render error messages from all of the above sources with consistent custom styles. import {Form, FieldError} from 'react-aria-components'; <Form> <DateRangePicker startName="startDate" endName="endDate" isRequired> <Label>Trip dates</Label> <Group> <DateInput slot="start"> {segment => <DateSegment segment={segment} />} </DateInput> <span aria-hidden="true">–</span> <DateInput slot="end"> {segment => <DateSegment segment={segment} />} </DateInput> <Button>▼</Button> </Group> <FieldError /> <Popover> <Dialog> <RangeCalendar> <header> <Button slot="previous">◀</Button> <Heading /> <Button slot="next">▶</Button> </header> <CalendarGrid> {date => <CalendarCell date={date} />} </CalendarGrid> </RangeCalendar> </Dialog> </Popover> </DateRangePicker> <Button type="submit">Submit</Button> </Form> import {FieldError, Form} from 'react-aria-components'; <Form> <DateRangePicker startName="startDate" endName="endDate" isRequired > <Label>Trip dates</Label> <Group> <DateInput slot="start"> {(segment) => <DateSegment segment={segment} />} </DateInput> <span aria-hidden="true">–</span> <DateInput slot="end"> {(segment) => <DateSegment segment={segment} />} </DateInput> <Button>▼</Button> </Group> <FieldError /> <Popover> <Dialog> <RangeCalendar> <header> <Button slot="previous">◀</Button> <Heading /> <Button slot="next">▶</Button> </header> <CalendarGrid> {(date) => <CalendarCell date={date} />} </CalendarGrid> </RangeCalendar> </Dialog> </Popover> </DateRangePicker> <Button type="submit">Submit</Button> </Form> import { FieldError, Form } from 'react-aria-components'; <Form> <DateRangePicker startName="startDate" endName="endDate" isRequired > <Label> Trip dates </Label> <Group> <DateInput slot="start"> {(segment) => ( <DateSegment segment={segment} /> )} </DateInput> <span aria-hidden="true"> – </span> <DateInput slot="end"> {(segment) => ( <DateSegment segment={segment} /> )} </DateInput> <Button> ▼ </Button> </Group> <FieldError /> <Popover> <Dialog> <RangeCalendar> <header> <Button slot="previous"> ◀ </Button> <Heading /> <Button slot="next"> ▶ </Button> </header> <CalendarGrid> {(date) => ( <CalendarCell date={date} /> )} </CalendarGrid> </RangeCalendar> </Dialog> </Popover> </DateRangePicker> <Button type="submit"> Submit </Button> </Form> Trip dates mm/dd/yyyy – mm/dd/yyyy ▼ Submit Show CSS .react-aria-DateRangePicker { &[data-invalid] { [slot=end]:after { content: '🚫' / ''; content: '🚫'; alt: ' '; flex: 1; text-align: end; margin-left: 1.5rem; margin-right: -1.5rem; } } .react-aria-FieldError { font-size: 12px; color: var(--invalid-color); } } .react-aria-DateRangePicker { &[data-invalid] { [slot=end]:after { content: '🚫' / ''; content: '🚫'; alt: ' '; flex: 1; text-align: end; margin-left: 1.5rem; margin-right: -1.5rem; } } .react-aria-FieldError { font-size: 12px; color: var(--invalid-color); } } .react-aria-DateRangePicker { &[data-invalid] { [slot=end]:after { content: '🚫' / ''; content: '🚫'; alt: ' '; flex: 1; text-align: end; margin-left: 1.5rem; margin-right: -1.5rem; } } .react-aria-FieldError { font-size: 12px; color: var(--invalid-color); } } By default, `FieldError` displays default validation messages provided by the browser. See Customizing error messages in the Forms guide to learn how to provide your own custom errors. ### Minimum and maximum values# The `minValue` and `maxValue` props can also be used to ensure the value is within a specific range. `DateRangePicker` also validates that the end date is after the start date. This example only accepts dates after today. import {today} from '@internationalized/date'; <Form> <MyDateRangePicker label="Trip dates" minValue={today(getLocalTimeZone())} defaultValue={{ start: parseDate('2022-02-03'), end: parseDate('2022-05-03') }} /> <Button type="submit">Submit</Button> </Form> import {today} from '@internationalized/date'; <Form> <MyDateRangePicker label="Trip dates" minValue={today(getLocalTimeZone())} defaultValue={{ start: parseDate('2022-02-03'), end: parseDate('2022-05-03') }} /> <Button type="submit">Submit</Button> </Form> import {today} from '@internationalized/date'; <Form> <MyDateRangePicker label="Trip dates" minValue={today( getLocalTimeZone() )} defaultValue={{ start: parseDate( '2022-02-03' ), end: parseDate( '2022-05-03' ) }} /> <Button type="submit"> Submit </Button> </Form> Trip dates 2/3/2022 – 5/3/2022 ▼ Submit ### Custom validation# The `validate` function can be used to perform custom validation logic. It receives the current date range, and should return a string or array of strings representing one or more error messages if the value is invalid. This example validates that the selected date range is a maximum of 1 week in duration. <Form> <MyDateRangePicker label="Trip dates" validate={(range) => range?.end.compare(range.start) > 7 ? 'Maximum stay duration is 1 week.' : null} defaultValue={{ start: today(getLocalTimeZone()), end: today(getLocalTimeZone()).add({ weeks: 1, days: 3 }) }} /> <Button type="submit">Submit</Button> </Form> <Form> <MyDateRangePicker label="Trip dates" validate={(range) => range?.end.compare(range.start) > 7 ? 'Maximum stay duration is 1 week.' : null} defaultValue={{ start: today(getLocalTimeZone()), end: today(getLocalTimeZone()).add({ weeks: 1, days: 3 }) }} /> <Button type="submit">Submit</Button> </Form> <Form> <MyDateRangePicker label="Trip dates" validate={(range) => range?.end .compare( range.start ) > 7 ? 'Maximum stay duration is 1 week.' : null} defaultValue={{ start: today( getLocalTimeZone() ), end: today( getLocalTimeZone() ).add({ weeks: 1, days: 3 }) }} /> <Button type="submit"> Submit </Button> </Form> Trip dates 6/18/2025 – 6/28/2025 ▼ Submit ### Unavailable dates# `DateRangePicker` supports marking certain dates as _unavailable_. These dates remain focusable with the keyboard in the calendar so that navigation is consistent, but cannot be selected by the user. The `isDateUnavailable` prop accepts a callback that is called to evaluate whether each visible date is unavailable. Note that by default, users may not select non-contiguous ranges, i.e. ranges that contain unavailable dates within them. Once a start date is selected in the calendar, enabled dates will be restricted to subsequent dates until an unavailable date is hit. While this is handled automatically in the calendar, additional validation logic must be provided to ensure an invalid state is displayed in the date field. This can be achieved using the `validate` prop. See below for an example of how to allow non-contiguous ranges. This example includes multiple unavailable date ranges, e.g. dates when a rental house is not available. The `minValue` prop is also used to prevent selecting dates before today. The `validate` prop is used to mark selected date ranges with unavailable dates in the middle as invalid. import {today} from '@internationalized/date'; function Example() { let now = today(getLocalTimeZone()); let disabledRanges = [ [now, now.add({ days: 5 })], [now.add({ days: 14 }), now.add({ days: 16 })], [now.add({ days: 23 }), now.add({ days: 24 })] ]; return ( <MyDateRangePicker label="Trip dates" minValue={today(getLocalTimeZone())} isDateUnavailable={(date) => disabledRanges.some((interval) => date.compare(interval[0]) >= 0 && date.compare(interval[1]) <= 0 )} validate={(value) => disabledRanges.some((interval) => value && value.end.compare(interval[0]) >= 0 && value.start.compare(interval[1]) <= 0 ) ? 'Selected date range may not include unavailable dates.' : null} /> ); } import {today} from '@internationalized/date'; function Example() { let now = today(getLocalTimeZone()); let disabledRanges = [ [now, now.add({ days: 5 })], [now.add({ days: 14 }), now.add({ days: 16 })], [now.add({ days: 23 }), now.add({ days: 24 })] ]; return ( <MyDateRangePicker label="Trip dates" minValue={today(getLocalTimeZone())} isDateUnavailable={(date) => disabledRanges.some((interval) => date.compare(interval[0]) >= 0 && date.compare(interval[1]) <= 0 )} validate={(value) => disabledRanges.some((interval) => value && value.end.compare(interval[0]) >= 0 && value.start.compare(interval[1]) <= 0 ) ? 'Selected date range may not include unavailable dates.' : null} /> ); } import {today} from '@internationalized/date'; function Example() { let now = today( getLocalTimeZone() ); let disabledRanges = [ [ now, now.add({ days: 5 }) ], [ now.add({ days: 14 }), now.add({ days: 16 }) ], [ now.add({ days: 23 }), now.add({ days: 24 }) ] ]; return ( <MyDateRangePicker label="Trip dates" minValue={today( getLocalTimeZone() )} isDateUnavailable={(date) => disabledRanges .some(( interval ) => date.compare( interval[ 0 ] ) >= 0 && date.compare( interval[ 1 ] ) <= 0 )} validate={(value) => disabledRanges .some( (interval) => value && value.end .compare( interval[ 0 ] ) >= 0 && value .start .compare( interval[ 1 ] ) <= 0 ) ? 'Selected date range may not include unavailable dates.' : null} /> ); } Trip dates mm/dd/yyyy – mm/dd/yyyy ▼ ### Non-contiguous ranges# The `allowsNonContiguousRanges` prop enables a range to be selected even if there are unavailable dates in the middle. The value emitted in the `onChange` event will still be a single range with a `start` and `end` property, but unavailable dates will not be displayed as selected. It is up to applications to split the full selected range into multiple as needed for business logic. This example prevents selecting weekends, but allows selecting ranges that span multiple weeks. import {useLocale} from 'react-aria'; import {isWeekend} from '@internationalized/date'; function Example() { let { locale } = useLocale(); return ( <MyDateRangePicker label="Time off request" isDateUnavailable={(date) => isWeekend(date, locale)} allowsNonContiguousRanges /> ); } import {useLocale} from 'react-aria'; import {isWeekend} from '@internationalized/date'; function Example() { let { locale } = useLocale(); return ( <MyDateRangePicker label="Time off request" isDateUnavailable={(date) => isWeekend(date, locale)} allowsNonContiguousRanges /> ); } import {useLocale} from 'react-aria'; import {isWeekend} from '@internationalized/date'; function Example() { let { locale } = useLocale(); return ( <MyDateRangePicker label="Time off request" isDateUnavailable={(date) => isWeekend( date, locale )} allowsNonContiguousRanges /> ); } Time off request mm/dd/yyyy – mm/dd/yyyy ▼ ### Description# The `description` slot can be used to associate additional help text with a date range picker. <DateRangePicker> <Label>Trip dates</Label> <Group> <DateInput slot="start"> {segment => <DateSegment segment={segment} />} </DateInput> <span aria-hidden="true">–</span> <DateInput slot="end"> {segment => <DateSegment segment={segment} />} </DateInput> <Button>▼</Button> </Group> <Text slot="description">Please your vacation dates.</Text> <Popover> <Dialog> <RangeCalendar> <header> <Button slot="previous">◀</Button> <Heading /> <Button slot="next">▶</Button> </header> <CalendarGrid> {date => <CalendarCell date={date} />} </CalendarGrid> </RangeCalendar> </Dialog> </Popover> </DateRangePicker> <DateRangePicker> <Label>Trip dates</Label> <Group> <DateInput slot="start"> {(segment) => <DateSegment segment={segment} />} </DateInput> <span aria-hidden="true">–</span> <DateInput slot="end"> {(segment) => <DateSegment segment={segment} />} </DateInput> <Button>▼</Button> </Group> <Text slot="description"> Please your vacation dates. </Text> <Popover> <Dialog> <RangeCalendar> <header> <Button slot="previous">◀</Button> <Heading /> <Button slot="next">▶</Button> </header> <CalendarGrid> {(date) => <CalendarCell date={date} />} </CalendarGrid> </RangeCalendar> </Dialog> </Popover> </DateRangePicker> <DateRangePicker> <Label> Trip dates </Label> <Group> <DateInput slot="start"> {(segment) => ( <DateSegment segment={segment} /> )} </DateInput> <span aria-hidden="true"> – </span> <DateInput slot="end"> {(segment) => ( <DateSegment segment={segment} /> )} </DateInput> <Button>▼</Button> </Group> <Text slot="description"> Please your vacation dates. </Text> <Popover> <Dialog> <RangeCalendar> <header> <Button slot="previous"> ◀ </Button> <Heading /> <Button slot="next"> ▶ </Button> </header> <CalendarGrid> {(date) => ( <CalendarCell date={date} /> )} </CalendarGrid> </RangeCalendar> </Dialog> </Popover> </DateRangePicker> Trip dates mm/dd/yyyy – mm/dd/yyyy ▼ Please your vacation dates. Show CSS .react-aria-DateRangePicker { [slot=description] { font-size: 12px; } } .react-aria-DateRangePicker { [slot=description] { font-size: 12px; } } .react-aria-DateRangePicker { [slot=description] { font-size: 12px; } } ## Format options# * * * ### Placeholder value# When no value is set, a placeholder is shown. The format of the placeholder is influenced by the `granularity` and `placeholderValue` props. `placeholderValue` also controls the default values of each segment when the user first interacts with them, e.g. using the up and down arrow keys, as well as the default month shown in the calendar popover. By default, the `placeholderValue` is the current date at midnight, but you can set it to a more appropriate value if needed. import {CalendarDate} from '@internationalized/date'; <MyDateRangePicker label="Date range" placeholderValue={new CalendarDate(1980, 1, 1)} /> import {CalendarDate} from '@internationalized/date'; <MyDateRangePicker label="Date range" placeholderValue={new CalendarDate(1980, 1, 1)} /> import {CalendarDate} from '@internationalized/date'; <MyDateRangePicker label="Date range" placeholderValue={new CalendarDate( 1980, 1, 1 )} /> Date range mm/dd/yyyy – mm/dd/yyyy ▼ ### Hide time zone# When `ZonedDateTime` objects are provided as the value of to `DateRangePicker`, the time zone abbreviation is displayed by default. However, if this is displayed elsewhere or implicit based on the usecase, it can be hidden using the `hideTimeZone` option. <MyDateRangePicker label="Date range" defaultValue={{ start: parseZonedDateTime('2022-11-07T10:45[America/Los_Angeles]'), end: parseZonedDateTime('2022-11-08T19:45[America/Los_Angeles]') }} hideTimeZone /> <MyDateRangePicker label="Date range" defaultValue={{ start: parseZonedDateTime( '2022-11-07T10:45[America/Los_Angeles]' ), end: parseZonedDateTime( '2022-11-08T19:45[America/Los_Angeles]' ) }} hideTimeZone /> <MyDateRangePicker label="Date range" defaultValue={{ start: parseZonedDateTime( '2022-11-07T10:45[America/Los_Angeles]' ), end: parseZonedDateTime( '2022-11-08T19:45[America/Los_Angeles]' ) }} hideTimeZone /> Date range 11/7/2022, 10:45 AM – 11/8/2022, 7:45 PM ▼ ### Hour cycle# By default, `DateRangePicker` displays times in either 12 or 24 hour hour format depending on the user's locale. However, this can be overridden using the `hourCycle` prop if needed for a specific usecase. This example forces the `DateRangePicker` to use 24-hour time, regardless of the locale. <MyDateRangePicker label="Date range" granularity="minute" hourCycle={24} /> <MyDateRangePicker label="Date range" granularity="minute" hourCycle={24} /> <MyDateRangePicker label="Date range" granularity="minute" hourCycle={24} /> Date range mm/dd/yyyy, ––:–– – mm/dd/yyyy, ––:–– ▼ ### Custom first day of week# By default, the first day of the week is automatically set based on the current locale. This can be changed by setting the `firstDayOfWeek` prop to `'sun'`, `'mon'`, `'tue'`, `'wed'`, `'thu'`, `'fri'`, or `'sat'`. <MyDateRangePicker label="Date range" firstDayOfWeek="mon" /> <MyDateRangePicker label="Date range" firstDayOfWeek="mon" /> <MyDateRangePicker label="Date range" firstDayOfWeek="mon" /> Date range mm/dd/yyyy – mm/dd/yyyy ▼ ## Props# * * * ### DateRangePicker# | Name | Type | Default | Description | | --- | --- | --- | --- | | `pageBehavior` | ` PageBehavior ` | `visible` | Controls the behavior of paging. Pagination either works by advancing the visible page by visibleDuration (default) or one unit of visibleDuration. | | `firstDayOfWeek` | `'sun' | 'mon' | 'tue' | 'wed' | 'thu' | 'fri' | 'sat'` | — | The day that starts the week. | | `minValue` | ` DateValue | null` | — | The minimum allowed date that a user may select. | | `maxValue` | ` DateValue | null` | — | The maximum allowed date that a user may select. | | `isDateUnavailable` | `( (date: DateValue )) => boolean` | — | Callback that is called for each date of the calendar. If it returns true, then the date is unavailable. | | `placeholderValue` | ` DateValue | null` | — | A placeholder date that influences the format of the placeholder shown when no value is selected. Defaults to today's date at midnight. | | `hourCycle` | `12 | 24` | — | Whether to display the time in 12 or 24 hour format. By default, this is determined by the user's locale. | | `granularity` | ` Granularity ` | — | Determines the smallest unit that is displayed in the date picker. By default, this is `"day"` for dates, and `"minute"` for times. | | `hideTimeZone` | `boolean` | `false` | Whether to hide the time zone abbreviation. | | `shouldForceLeadingZeros` | `boolean` | — | Whether to always show leading zeros in the month, day, and hour fields. By default, this is determined by the user's locale. | | `isDisabled` | `boolean` | — | Whether the input is disabled. | | `isReadOnly` | `boolean` | — | Whether the input can be selected but not changed by the user. | | `isRequired` | `boolean` | — | Whether user input is required on the input before form submission. | | `isInvalid` | `boolean` | — | Whether the input value is invalid. | | `autoFocus` | `boolean` | — | Whether the element should receive focus on render. | | `isOpen` | `boolean` | — | Whether the overlay is open by default (controlled). | | `defaultOpen` | `boolean` | — | Whether the overlay is open by default (uncontrolled). | | `allowsNonContiguousRanges` | `boolean` | — | When combined with `isDateUnavailable`, determines whether non-contiguous ranges, i.e. ranges containing unavailable dates, may be selected. | | `startName` | `string` | — | The name of the start date input element, used when submitting an HTML form. See MDN. | | `endName` | `string` | — | The name of the end date input element, used when submitting an HTML form. See MDN. | | `validate` | `( (value: RangeValue < MappedDateValue < DateValue >> )) => ValidationError | true | null | undefined` | — | A function that returns an error message if a given value is invalid. Validation errors are displayed to the user when the form is submitted if `validationBehavior="native"`. For realtime validation, use the `isInvalid` prop instead. | | `value` | ` RangeValue < DateValue > | null` | — | The current value (controlled). | | `defaultValue` | ` RangeValue < DateValue > | null` | — | The default value (uncontrolled). | | `shouldCloseOnSelect` | `boolean | () => boolean` | `true` | Determines whether the date picker popover should close automatically when a date is selected. | | `validationBehavior` | `'native' | 'aria'` | `'native'` | Whether to use native HTML form validation to prevent form submission when the value is missing or invalid, or mark the field as required or invalid via ARIA. | | `children` | `ReactNode | ( (values: DateRangePickerRenderProps & & { defaultChildren: ReactNode | | undefined } )) => ReactNode` | — | The children of the component. A function may be provided to alter the children based on component state. | | `className` | `string | ( (values: DateRangePickerRenderProps & & { defaultClassName: string | | undefined } )) => string` | — | The CSS className for the element. A function may be provided to compute the class based on component state. | | `style` | `CSSProperties | ( (values: DateRangePickerRenderProps & & { defaultStyle: CSSProperties } )) => CSSProperties | undefined` | — | The inline style for the element. A function may be provided to compute the style based on component state. | Events | Name | Type | Description | | --- | --- | --- | | `onFocus` | `( (e: FocusEvent<Target> )) => void` | Handler that is called when the element receives focus. | | `onBlur` | `( (e: FocusEvent<Target> )) => void` | Handler that is called when the element loses focus. | | `onFocusChange` | `( (isFocused: boolean )) => void` | Handler that is called when the element's focus status changes. | | `onKeyDown` | `( (e: KeyboardEvent )) => void` | Handler that is called when a key is pressed. | | `onKeyUp` | `( (e: KeyboardEvent )) => void` | Handler that is called when a key is released. | | `onOpenChange` | `( (isOpen: boolean )) => void` | Handler that is called when the overlay's open state changes. | | `onChange` | `( (value: RangeValue < MappedDateValue < DateValue >> | | null )) => void` | Handler that is called when the value changes. | Layout | Name | Type | Description | | --- | --- | --- | | `slot` | `string | null` | A slot name for the component. Slots allow the component to receive props from a parent component. An explicit `null` value indicates that the local props completely override all props received from a parent. | Accessibility | Name | Type | Description | | --- | --- | --- | | `id` | `string` | The element's unique identifier. See MDN. | | `aria-label` | `string` | Defines a string value that labels the current element. | | `aria-labelledby` | `string` | Identifies the element (or elements) that labels the current element. | | `aria-describedby` | `string` | Identifies the element (or elements) that describes the object. | | `aria-details` | `string` | Identifies the element (or elements) that provide a detailed, extended description for the object. | ### Label# A `<Label>` accepts all HTML attributes. ### Group# A `<Group>` accepts all HTML attributes. ### DateInput# The `<DateInput>` component renders a group of date segments. It accepts a function as its `children`, which is called to render a `<DateSegment>` for each segment. The `slot="start"` and `slot="end"` props distinguish the two instances within a `DateRangePicker`. Show props | Name | Type | Description | | --- | --- | --- | | `children` | `( (segment: DateSegment )) => ReactElement` | | | `className` | `string | ( (values: DateInputRenderProps & & { defaultClassName: string | | undefined } )) => string` | The CSS className for the element. A function may be provided to compute the class based on component state. | | `style` | `CSSProperties | ( (values: DateInputRenderProps & & { defaultStyle: CSSProperties } )) => CSSProperties | undefined` | The inline style for the element. A function may be provided to compute the style based on component state. | Layout | Name | Type | Description | | --- | --- | --- | | `slot` | `string | null` | A slot name for the component. Slots allow the component to receive props from a parent component. An explicit `null` value indicates that the local props completely override all props received from a parent. | ### DateSegment# The `<DateSegment>` component renders an individual segment. Show props | Name | Type | Description | | --- | --- | --- | | `segment` | ` DateSegment ` | | | `children` | `ReactNode | ( (values: DateSegmentRenderProps & & { defaultChildren: ReactNode | | undefined } )) => ReactNode` | The children of the component. A function may be provided to alter the children based on component state. | | `className` | `string | ( (values: DateSegmentRenderProps & & { defaultClassName: string | | undefined } )) => string` | The CSS className for the element. A function may be provided to compute the class based on component state. | | `style` | `CSSProperties | ( (values: DateSegmentRenderProps & & { defaultStyle: CSSProperties } )) => CSSProperties | undefined` | The inline style for the element. A function may be provided to compute the style based on component state. | Events | Name | Type | Description | | --- | --- | --- | | `onHoverStart` | `( (e: HoverEvent )) => void` | Handler that is called when a hover interaction starts. | | `onHoverEnd` | `( (e: HoverEvent )) => void` | Handler that is called when a hover interaction ends. | | `onHoverChange` | `( (isHovering: boolean )) => void` | Handler that is called when the hover state changes. | ### Button# A `<Button>` accepts its contents as `children`. Other props such as `onPress` and `isDisabled` will be set by the `DateRangePicker` or `RangeCalendar`. Show props | Name | Type | Default | Description | | --- | --- | --- | --- | | `form` | `string` | — | The `<form>` element to associate the button with. The value of this attribute must be the id of a `<form>` in the same document. | | `formAction` | `string` | — | The URL that processes the information submitted by the button. Overrides the action attribute of the button's form owner. | | `formEncType` | `string` | — | Indicates how to encode the form data that is submitted. | | `formMethod` | `string` | — | Indicates the HTTP method used to submit the form. | | `formNoValidate` | `boolean` | — | Indicates that the form is not to be validated when it is submitted. | | `formTarget` | `string` | — | Overrides the target attribute of the button's form owner. | | `name` | `string` | — | Submitted as a pair with the button's value as part of the form data. | | `value` | `string` | — | The value associated with the button's name when it's submitted with the form data. | | `isPending` | `boolean` | — | Whether the button is in a pending state. This disables press and hover events while retaining focusability, and announces the pending state to screen readers. | | `isDisabled` | `boolean` | — | Whether the button is disabled. | | `autoFocus` | `boolean` | — | Whether the element should receive focus on render. | | `type` | `'button' | 'submit' | 'reset'` | `'button'` | The behavior of the button when used in an HTML form. | | `children` | `ReactNode | ( (values: ButtonRenderProps & & { defaultChildren: ReactNode | | undefined } )) => ReactNode` | — | The children of the component. A function may be provided to alter the children based on component state. | | `className` | `string | ( (values: ButtonRenderProps & & { defaultClassName: string | | undefined } )) => string` | — | The CSS className for the element. A function may be provided to compute the class based on component state. | | `style` | `CSSProperties | ( (values: ButtonRenderProps & & { defaultStyle: CSSProperties } )) => CSSProperties | undefined` | — | The inline style for the element. A function may be provided to compute the style based on component state. | Events | Name | Type | Description | | --- | --- | --- | | `onPress` | `( (e: PressEvent )) => void` | Handler that is called when the press is released over the target. | | `onPressStart` | `( (e: PressEvent )) => void` | Handler that is called when a press interaction starts. | | `onPressEnd` | `( (e: PressEvent )) => void` | Handler that is called when a press interaction ends, either over the target or when the pointer leaves the target. | | `onPressChange` | `( (isPressed: boolean )) => void` | Handler that is called when the press state changes. | | `onPressUp` | `( (e: PressEvent )) => void` | Handler that is called when a press is released over the target, regardless of whether it started on the target or not. | | `onClick` | `( (e: MouseEvent<FocusableElement> )) => void` | **Not recommended – use `onPress` instead.** `onClick` is an alias for `onPress` provided for compatibility with other libraries. `onPress` provides additional event details for non-mouse interactions. | | `onFocus` | `( (e: FocusEvent<Target> )) => void` | Handler that is called when the element receives focus. | | `onBlur` | `( (e: FocusEvent<Target> )) => void` | Handler that is called when the element loses focus. | | `onFocusChange` | `( (isFocused: boolean )) => void` | Handler that is called when the element's focus status changes. | | `onKeyDown` | `( (e: KeyboardEvent )) => void` | Handler that is called when a key is pressed. | | `onKeyUp` | `( (e: KeyboardEvent )) => void` | Handler that is called when a key is released. | | `onHoverStart` | `( (e: HoverEvent )) => void` | Handler that is called when a hover interaction starts. | | `onHoverEnd` | `( (e: HoverEvent )) => void` | Handler that is called when a hover interaction ends. | | `onHoverChange` | `( (isHovering: boolean )) => void` | Handler that is called when the hover state changes. | Layout | Name | Type | Description | | --- | --- | --- | | `slot` | `string | null` | A slot name for the component. Slots allow the component to receive props from a parent component. An explicit `null` value indicates that the local props completely override all props received from a parent. | Accessibility | Name | Type | Description | | --- | --- | --- | | `id` | `string` | The element's unique identifier. See MDN. | | `excludeFromTabOrder` | `boolean` | Whether to exclude the element from the sequential tab order. If true, the element will not be focusable via the keyboard by tabbing. This should be avoided except in rare scenarios where an alternative means of accessing the element or its functionality via the keyboard is available. | | `preventFocusOnPress` | `boolean` | Whether to prevent focus from moving to the button when pressing it. Caution, this can make the button inaccessible and should only be used when alternative keyboard interaction is provided, such as ComboBox's MenuTrigger or a NumberField's increment/decrement control. | | `aria-expanded` | `boolean | 'true' | 'false'` | Indicates whether the element, or another grouping element it controls, is currently expanded or collapsed. | | `aria-haspopup` | `boolean | 'menu' | 'listbox' | 'tree' | 'grid' | 'dialog' | 'true' | 'false'` | Indicates the availability and type of interactive popup element, such as menu or dialog, that can be triggered by an element. | | `aria-controls` | `string` | Identifies the element (or elements) whose contents or presence are controlled by the current element. | | `aria-pressed` | `boolean | 'true' | 'false' | 'mixed'` | Indicates the current "pressed" state of toggle buttons. | | `aria-current` | `boolean | 'true' | 'false' | 'page' | 'step' | 'location' | 'date' | 'time'` | Indicates whether this element represents the current item within a container or set of related elements. | | `aria-label` | `string` | Defines a string value that labels the current element. | | `aria-labelledby` | `string` | Identifies the element (or elements) that labels the current element. | | `aria-describedby` | `string` | Identifies the element (or elements) that describes the object. | | `aria-details` | `string` | Identifies the element (or elements) that provide a detailed, extended description for the object. | ### FieldError# A `<FieldError>` displays validation errors. Show props | Name | Type | Description | | --- | --- | --- | | `children` | `ReactNode | ( (values: FieldErrorRenderProps & & { defaultChildren: ReactNode | | undefined } )) => ReactNode` | The children of the component. A function may be provided to alter the children based on component state. | | `className` | `string | ( (values: FieldErrorRenderProps & & { defaultClassName: string | | undefined } )) => string` | The CSS className for the element. A function may be provided to compute the class based on component state. | | `style` | `CSSProperties | ( (values: FieldErrorRenderProps & & { defaultStyle: CSSProperties } )) => CSSProperties | undefined` | The inline style for the element. A function may be provided to compute the style based on component state. | Accessibility | Name | Type | Description | | --- | --- | --- | | `id` | `string` | The element's unique identifier. See MDN. | ### Popover# A `<Popover>` is an overlay to hold the `<Calendar>`, positioned relative to the `<Group>`. By default, it has a `placement` of `bottom start` within a `<DateRangePicker>`, but this and other positioning properties may be customized. Show props | Name | Type | Default | Description | | --- | --- | --- | --- | | `trigger` | `string` | — | The name of the component that triggered the popover. This is reflected on the element as the `data-trigger` attribute, and can be used to provide specific styles for the popover depending on which element triggered it. | | `triggerRef` | ` RefObject <Element | null>` | — | The ref for the element which the popover positions itself with respect to. When used within a trigger component such as DialogTrigger, MenuTrigger, Select, etc., this is set automatically. It is only required when used standalone. | | `isEntering` | `boolean` | — | Whether the popover is currently performing an entry animation. | | `isExiting` | `boolean` | — | Whether the popover is currently performing an exit animation. | | `offset` | `number` | `8` | The additional offset applied along the main axis between the element and its anchor element. | | `placement` | ` Placement ` | `'bottom'` | The placement of the element with respect to its anchor element. | | `containerPadding` | `number` | `12` | The placement padding that should be applied between the element and its surrounding container. | | `crossOffset` | `number` | `0` | The additional offset applied along the cross axis between the element and its anchor element. | | `shouldFlip` | `boolean` | `true` | Whether the element should flip its orientation (e.g. top to bottom or left to right) when there is insufficient room for it to render completely. | | `isNonModal` | `boolean` | — | Whether the popover is non-modal, i.e. elements outside the popover may be interacted with by assistive technologies. Most popovers should not use this option as it may negatively impact the screen reader experience. Only use with components such as combobox, which are designed to handle this situation carefully. | | `isKeyboardDismissDisabled` | `boolean` | `false` | Whether pressing the escape key to close the popover should be disabled. Most popovers should not use this option. When set to true, an alternative way to close the popover with a keyboard must be provided. | | `shouldCloseOnInteractOutside` | `( (element: Element )) => boolean` | — | When user interacts with the argument element outside of the popover ref, return true if onClose should be called. This gives you a chance to filter out interaction with elements that should not dismiss the popover. By default, onClose will always be called on interaction outside the popover ref. | | `boundaryElement` | `Element` | `document.body` | Element that that serves as the positioning boundary. | | `scrollRef` | ` RefObject <Element | null>` | `overlayRef` | A ref for the scrollable region within the overlay. | | `shouldUpdatePosition` | `boolean` | `true` | Whether the overlay should update its position automatically. | | `arrowBoundaryOffset` | `number` | `0` | The minimum distance the arrow's edge should be from the edge of the overlay element. | | `isOpen` | `boolean` | — | Whether the overlay is open by default (controlled). | | `defaultOpen` | `boolean` | — | Whether the overlay is open by default (uncontrolled). | | `children` | `ReactNode | ( (values: PopoverRenderProps & & { defaultChildren: ReactNode | | undefined } )) => ReactNode` | — | The children of the component. A function may be provided to alter the children based on component state. | | `className` | `string | ( (values: PopoverRenderProps & & { defaultClassName: string | | undefined } )) => string` | — | The CSS className for the element. A function may be provided to compute the class based on component state. | | `style` | `CSSProperties | ( (values: PopoverRenderProps & & { defaultStyle: CSSProperties } )) => CSSProperties | undefined` | — | The inline style for the element. A function may be provided to compute the style based on component state. | Events | Name | Type | Description | | --- | --- | --- | | `onOpenChange` | `( (isOpen: boolean )) => void` | Handler that is called when the overlay's open state changes. | Layout | Name | Type | Description | | --- | --- | --- | | `slot` | `string | null` | A slot name for the component. Slots allow the component to receive props from a parent component. An explicit `null` value indicates that the local props completely override all props received from a parent. | Sizing | Name | Type | Description | | --- | --- | --- | | `maxHeight` | `number` | The maxHeight specified for the overlay element. By default, it will take all space up to the current viewport height. | Accessibility | Name | Type | Description | | --- | --- | --- | | `aria-label` | `string` | Defines a string value that labels the current element. | | `aria-labelledby` | `string` | Identifies the element (or elements) that labels the current element. | | `aria-describedby` | `string` | Identifies the element (or elements) that describes the object. | | `aria-details` | `string` | Identifies the element (or elements) that provide a detailed, extended description for the object. | ### Dialog# A `<Dialog>` is placed within a `<Popover>` to hold the `<Calendar>` for a DateRangePicker. Show props | Name | Type | Description | | --- | --- | --- | | `children` | `ReactNode | ( (opts: DialogRenderProps )) => ReactNode` | Children of the dialog. A function may be provided to access a function to close the dialog. | | `className` | `string` | The CSS className for the element. | | `style` | `CSSProperties` | The inline style for the element. | Layout | Name | Type | Description | | --- | --- | --- | | `slot` | `string | null` | A slot name for the component. Slots allow the component to receive props from a parent component. An explicit `null` value indicates that the local props completely override all props received from a parent. | Accessibility | Name | Type | Default | Description | | --- | --- | --- | --- | | `role` | `'dialog' | 'alertdialog'` | `'dialog'` | The accessibility role for the dialog. | | `id` | `string` | — | The element's unique identifier. See MDN. | | `aria-label` | `string` | — | Defines a string value that labels the current element. | | `aria-labelledby` | `string` | — | Identifies the element (or elements) that labels the current element. | | `aria-describedby` | `string` | — | Identifies the element (or elements) that describes the object. | | `aria-details` | `string` | — | Identifies the element (or elements) that provide a detailed, extended description for the object. | ### RangeCalendar# A `<RangeCalendar>` accepts one or more month grids as children, along with previous and next buttons to navigate forward and backward. Show props | Name | Type | Default | Description | | --- | --- | --- | --- | | `visibleDuration` | ` DateDuration ` | `{months: 1}` | The amount of days that will be displayed at once. This affects how pagination works. | | `createCalendar` | `( (identifier: CalendarIdentifier )) => Calendar ` | — | A function to create a new Calendar object for a given calendar identifier. If not provided, the `createCalendar` function from `@internationalized/date` will be used. | | `minValue` | ` DateValue | null` | — | The minimum allowed date that a user may select. | | `maxValue` | ` DateValue | null` | — | The maximum allowed date that a user may select. | | `isDateUnavailable` | `( (date: DateValue )) => boolean` | — | Callback that is called for each date of the calendar. If it returns true, then the date is unavailable. | | `isDisabled` | `boolean` | `false` | Whether the calendar is disabled. | | `isReadOnly` | `boolean` | `false` | Whether the calendar value is immutable. | | `autoFocus` | `boolean` | `false` | Whether to automatically focus the calendar when it mounts. | | `focusedValue` | ` DateValue | null` | — | Controls the currently focused date within the calendar. | | `defaultFocusedValue` | ` DateValue | null` | — | The date that is focused when the calendar first mounts (uncountrolled). | | `isInvalid` | `boolean` | — | Whether the current selection is invalid according to application logic. | | `pageBehavior` | ` PageBehavior ` | `visible` | Controls the behavior of paging. Pagination either works by advancing the visible page by visibleDuration (default) or one unit of visibleDuration. | | `firstDayOfWeek` | `'sun' | 'mon' | 'tue' | 'wed' | 'thu' | 'fri' | 'sat'` | — | The day that starts the week. | | `value` | ` DateValue | null` | — | The current value (controlled). | | `defaultValue` | ` DateValue | null` | — | The default value (uncontrolled). | | `children` | `ReactNode | ( (values: CalendarRenderProps & & { defaultChildren: ReactNode | | undefined } )) => ReactNode` | — | The children of the component. A function may be provided to alter the children based on component state. | | `className` | `string | ( (values: CalendarRenderProps & & { defaultClassName: string | | undefined } )) => string` | — | The CSS className for the element. A function may be provided to compute the class based on component state. | | `style` | `CSSProperties | ( (values: CalendarRenderProps & & { defaultStyle: CSSProperties } )) => CSSProperties | undefined` | — | The inline style for the element. A function may be provided to compute the style based on component state. | Events | Name | Type | Description | | --- | --- | --- | | `onFocusChange` | `( (date: CalendarDate )) => void` | Handler that is called when the focused date changes. | | `onChange` | `( (value: MappedDateValue < DateValue > )) => void` | Handler that is called when the value changes. | Layout | Name | Type | Description | | --- | --- | --- | | `slot` | `string | null` | A slot name for the component. Slots allow the component to receive props from a parent component. An explicit `null` value indicates that the local props completely override all props received from a parent. | Accessibility | Name | Type | Description | | --- | --- | --- | | `id` | `string` | The element's unique identifier. See MDN. | | `aria-label` | `string` | Defines a string value that labels the current element. | | `aria-labelledby` | `string` | Identifies the element (or elements) that labels the current element. | | `aria-describedby` | `string` | Identifies the element (or elements) that describes the object. | | `aria-details` | `string` | Identifies the element (or elements) that provide a detailed, extended description for the object. | ### CalendarGrid# A `<CalendarGrid>` renders an individual month within a `<RangeCalendar>`. It accepts a function as its `children`, which is called to render a `<CalendarCell>` for each date. This renders a default `<CalendarGridHeader>`, which displays the weekday names in the column headers. This can be customized by providing a `<CalendarGridHeader>` and `<CalendarGridBody>` as children instead of a function. Show props | Name | Type | Default | Description | | --- | --- | --- | --- | | `children` | `ReactElement | ReactElement[] | ( (date: CalendarDate )) => ReactElement` | — | Either a function to render calendar cells for each date in the month, or children containing a `<CalendarGridHeader>`` and`<CalendarGridBody>\` when additional customization is needed. | | `offset` | ` DateDuration ` | — | An offset from the beginning of the visible date range that this CalendarGrid should display. Useful when displaying more than one month at a time. | | `weekdayStyle` | `'narrow' | 'short' | 'long'` | `"narrow"` | The style of weekday names to display in the calendar grid header, e.g. single letter, abbreviation, or full day name. | | `className` | `string` | — | The CSS className for the element. | | `style` | `CSSProperties` | — | The inline style for the element. | ### CalendarGridHeader# A `<CalendarGridHeader>` renders the header within a `<CalendarGrid>`, displaying a list of weekday names. It accepts a function as its `children`, which is called with a day name abbreviation to render a `<CalendarHeaderCell>` for each column header. Show props | Name | Type | Description | | --- | --- | --- | | `children` | `( (day: string )) => ReactElement` | A function to render a `<CalendarHeaderCell>` for a weekday name. | | `className` | `string` | The CSS className for the element. | | `style` | `CSSProperties` | The inline style for the element. | ### CalendarHeaderCell# A `<CalendarHeaderCell>` renders a column header within a `<CalendarGridHeader>`. It typically displays a weekday name. Show props | Name | Type | Description | | --- | --- | --- | | `children` | `ReactNode` | The children of the component. | | `className` | `string` | The CSS className for the element. | | `style` | `CSSProperties` | The inline style for the element. | Accessibility | Name | Type | Description | | --- | --- | --- | | `id` | `string` | The element's unique identifier. See MDN. | ### CalendarGridBody# A `<CalendarGridBody>` renders the body within a `<CalendarGrid>`. It accepts a function as its `children`, which is called to render a `<CalendarCell>` for each date. Show props | Name | Type | Description | | --- | --- | --- | | `children` | `( (date: CalendarDate )) => ReactElement` | A function to render a `<CalendarCell>` for a given date. | | `className` | `string` | The CSS className for the element. | | `style` | `CSSProperties` | The inline style for the element. | ### CalendarCell# A `<CalendarCell>` renders an individual date within a `<CalendarGrid>`. Show props | Name | Type | Description | | --- | --- | --- | | `date` | ` CalendarDate ` | The date to render in the cell. | | `children` | `ReactNode | ( (values: CalendarCellRenderProps & & { defaultChildren: ReactNode | | undefined } )) => ReactNode` | The children of the component. A function may be provided to alter the children based on component state. | | `className` | `string | ( (values: CalendarCellRenderProps & & { defaultClassName: string | | undefined } )) => string` | The CSS className for the element. A function may be provided to compute the class based on component state. | | `style` | `CSSProperties | ( (values: CalendarCellRenderProps & & { defaultStyle: CSSProperties } )) => CSSProperties | undefined` | The inline style for the element. A function may be provided to compute the style based on component state. | Events | Name | Type | Description | | --- | --- | --- | | `onHoverStart` | `( (e: HoverEvent )) => void` | Handler that is called when a hover interaction starts. | | `onHoverEnd` | `( (e: HoverEvent )) => void` | Handler that is called when a hover interaction ends. | | `onHoverChange` | `( (isHovering: boolean )) => void` | Handler that is called when the hover state changes. | ## Styling# * * * React Aria components can be styled in many ways, including using CSS classes, inline styles, utility classes (e.g. Tailwind), CSS-in-JS (e.g. Styled Components), etc. By default, all components include a builtin `className` attribute which can be targeted using CSS selectors. These follow the `react-aria-ComponentName` naming convention. .react-aria-DateRangePicker { /* ... */ } .react-aria-DateRangePicker { /* ... */ } .react-aria-DateRangePicker { /* ... */ } A custom `className` can also be specified on any component. This overrides the default `className` provided by React Aria with your own. <DateInput className="my-date-input"> {/* ... */} </DateInput> <DateInput className="my-date-input"> {/* ... */} </DateInput> <DateInput className="my-date-input"> {/* ... */} </DateInput> In addition, some components support multiple UI states (e.g. focused, placeholder, readonly, etc.). React Aria components expose states using data attributes, which you can target in CSS selectors. For example: .react-aria-DateSegment[data-placeholder] { /* ... */ } .react-aria-DateSegment[data-readonly] { /* ... */ } .react-aria-DateSegment[data-placeholder] { /* ... */ } .react-aria-DateSegment[data-readonly] { /* ... */ } .react-aria-DateSegment[data-placeholder] { /* ... */ } .react-aria-DateSegment[data-readonly] { /* ... */ } The `className` and `style` props also accept functions which receive states for styling. This lets you dynamically determine the classes or styles to apply, which is useful when using utility CSS libraries like Tailwind. <DateSegment className={({ isPlaceholder }) => isPlaceholder ? 'bg-gray-300' : 'bg-gray-600'} /> <DateSegment className={({ isPlaceholder }) => isPlaceholder ? 'bg-gray-300' : 'bg-gray-600'} /> <DateSegment className={( { isPlaceholder } ) => isPlaceholder ? 'bg-gray-300' : 'bg-gray-600'} /> Render props may also be used as children to alter what elements are rendered based on the current state. For example, you could render the placeholder as a separate element to always reserve space for it. <DateSegment> {({ text, placeholder, isPlaceholder }) => ( <> <span style={{ visibility: isPlaceholder ? 'visible' : 'hidden' }}> {placeholder} </span> {isPlaceholder ? '' : text} </> )} </DateSegment> <DateSegment> {({ text, placeholder, isPlaceholder }) => ( <> <span style={{ visibility: isPlaceholder ? 'visible' : 'hidden' }} > {placeholder} </span> {isPlaceholder ? '' : text} </> )} </DateSegment> <DateSegment> {( { text, placeholder, isPlaceholder } ) => ( <> <span style={{ visibility: isPlaceholder ? 'visible' : 'hidden' }} > {placeholder} </span> {isPlaceholder ? '' : text} </> )} </DateSegment> The states, selectors, and render props for each component used in a `DateRangePicker` are documented below. ### DateRangePicker# A `DateRangePicker` can be targeted with the `.react-aria-DateRangePicker` CSS selector, or by overriding with a custom `className`. It supports the following states and render props: | Name | CSS Selector | Description | | --- | --- | --- | | `state` | `—` | State of the date range picker. | | `isFocusWithin` | `[data-focus-within]` | Whether an element within the date picker is focused, either via a mouse or keyboard. | | `isFocusVisible` | `[data-focus-visible]` | Whether an element within the date picker is keyboard focused. | | `isDisabled` | `[data-disabled]` | Whether the date picker is disabled. | | `isInvalid` | `[data-invalid]` | Whether the date picker is invalid. | | `isOpen` | `[data-open]` | Whether the date picker's popover is currently open. | ### Label# A `Label` can be targeted with the `.react-aria-Label` CSS selector, or by overriding with a custom `className`. ### Group# A `Group` can be targeted with the `.react-aria-Group` selector, or by overriding with a custom `className`. It supports the following states: | Name | CSS Selector | Description | | --- | --- | --- | | `isHovered` | `[data-hovered]` | Whether the group is currently hovered with a mouse. | | `isFocusWithin` | `[data-focus-within]` | Whether an element within the group is focused, either via a mouse or keyboard. | | `isFocusVisible` | `[data-focus-visible]` | Whether an element within the group is keyboard focused. | | `isDisabled` | `[data-disabled]` | Whether the group is disabled. | | `isInvalid` | `[data-invalid]` | Whether the group is invalid. | ### DateInput# A `DateInput` can be targeted with the `.react-aria-DateInput` CSS selector, or by overriding with a custom `className`. It supports the following states: | Name | CSS Selector | Description | | --- | --- | --- | | `isHovered` | `[data-hovered]` | Whether the date input is currently hovered with a mouse. | | `isFocusWithin` | `[data-focus-within]` | Whether an element within the date input is focused, either via a mouse or keyboard. | | `isFocusVisible` | `[data-focus-visible]` | Whether an element within the date input is keyboard focused. | | `isDisabled` | `[data-disabled]` | Whether the date input is disabled. | | `isInvalid` | `[data-invalid]` | Whether the date input is invalid. | ### DateSegment# A `DateSegment` can be targeted with the `.react-aria-DateSegment` CSS selector, or by overriding with a custom `className`. It supports the following states and render props: | Name | CSS Selector | Description | | --- | --- | --- | | `isHovered` | `[data-hovered]` | Whether the segment is currently hovered with a mouse. | | `isFocused` | `[data-focused]` | Whether the segment is focused, either via a mouse or keyboard. | | `isFocusVisible` | `[data-focus-visible]` | Whether the segment is keyboard focused. | | `isPlaceholder` | `[data-placeholder]` | Whether the value is a placeholder. | | `isReadOnly` | `[data-readonly]` | Whether the segment is read only. | | `isDisabled` | `[data-disabled]` | Whether the date field is disabled. | | `isInvalid` | `[data-invalid]` | Whether the date field is in an invalid state. | | `type` | `[data-type="..."]` | The type of segment. Values include `literal`, `year`, `month`, `day`, etc. | | `text` | `—` | The formatted text for the segment. | | `placeholder` | `—` | A placeholder string for the segment. | ### Button# A Button can be targeted with the `.react-aria-Button` CSS selector, or by overriding with a custom `className`. The next and previous buttons within a range calendar can be targeted specifically with the `[slot=previous]` and `[slot=next]` selectors. Buttons support the following states: | Name | CSS Selector | Description | | --- | --- | --- | | `isHovered` | `[data-hovered]` | Whether the button is currently hovered with a mouse. | | `isPressed` | `[data-pressed]` | Whether the button is currently in a pressed state. | | `isFocused` | `[data-focused]` | Whether the button is focused, either via a mouse or keyboard. | | `isFocusVisible` | `[data-focus-visible]` | Whether the button is keyboard focused. | | `isDisabled` | `[data-disabled]` | Whether the button is disabled. | | `isPending` | `[data-pending]` | Whether the button is currently in a pending state. | ### FieldError# A `FieldError` can be targeted with the `.react-aria-FieldError` CSS selector, or by overriding with a custom `className`. It supports the following render props: | Name | Description | | --- | --- | | `isInvalid` | Whether the input value is invalid. | | `validationErrors` | The current error messages for the input if it is invalid, otherwise an empty array. | | `validationDetails` | The native validation details for the input. | ### Popover# The Popover component can be targeted with the `.react-aria-Popover` CSS selector, or by overriding with a custom `className`. Note that it renders in a React Portal, so it will not appear as a descendant of the DateRangePicker in the DOM. It supports the following states and render props: | Name | CSS Selector | Description | | --- | --- | --- | | `trigger` | `[data-trigger="..."]` | The name of the component that triggered the popover, e.g. "DialogTrigger" or "ComboBox". | | `placement` | `[data-placement="left | right | top | bottom"]` | The placement of the popover relative to the trigger. | | `isEntering` | `[data-entering]` | Whether the popover is currently entering. Use this to apply animations. | | `isExiting` | `[data-exiting]` | Whether the popover is currently exiting. Use this to apply animations. | Within a DateRangePicker, the popover will have the `data-trigger="DateRangePicker"` attribute, which can be used to define date range picker-specific styles. In addition, the `--trigger-width` CSS custom property will be set on the popover, which you can use to make the popover match the width of the input group. .react-aria-Popover[data-trigger=DateRangePicker] { width: var(--trigger-width); } .react-aria-Popover[data-trigger=DateRangePicker] { width: var(--trigger-width); } .react-aria-Popover[data-trigger=DateRangePicker] { width: var(--trigger-width); } ### Dialog# A Dialog can be targeted with the `.react-aria-Dialog` CSS selector, or by overriding with a custom `className`. ### RangeCalendar# A RangeCalendar can be targeted with the `.react-aria-RangeCalendar` CSS selector, or by overriding with a custom `className`. ### CalendarGrid# A `CalendarGrid` can be targeted with the `.react-aria-CalendarGrid` CSS selector, or by overriding with a custom `className`. When a function is provided as children, a default `<CalendarGridHeader>` and `<CalendarGridBody>` are rendered. If you need to customize the styling of the header cells, you can render them yourself. See the RangeCalendar docs for an example. ### CalendarGridHeader# A `CalendarGridHeader` can be targeted with the `.react-aria-CalendarGridHeader` CSS selector, or by overriding with a custom `className`. ### CalendarHeaderCell# A `CalendarHeaderCell` can be targeted with the `.react-aria-CalendarHeaderCell` CSS selector, or by overriding with a custom `className`. ### CalendarGridBody# A `CalendarGridBody` can be targeted with the `.react-aria-CalendarGridBody` CSS selector, or by overriding with a custom `className`. ### CalendarCell# A `CalendarCell` can be targeted with the `.react-aria-CalendarCell` CSS selector, or by overriding with a custom `className`. It supports the following states: | Name | CSS Selector | Description | | --- | --- | --- | | `date` | `—` | The date that the cell represents. | | `formattedDate` | `—` | The day number formatted according to the current locale. | | `isHovered` | `[data-hovered]` | Whether the cell is currently hovered with a mouse. | | `isPressed` | `[data-pressed]` | Whether the cell is currently being pressed. | | `isSelected` | `[data-selected]` | Whether the cell is selected. | | `isSelectionStart` | `[data-selection-start]` | Whether the cell is the first date in a range selection. | | `isSelectionEnd` | `[data-selection-end]` | Whether the cell is the last date in a range selection. | | `isFocused` | `[data-focused]` | Whether the cell is focused. | | `isFocusVisible` | `[data-focus-visible]` | Whether the cell is keyboard focused. | | `isDisabled` | `[data-disabled]` | Whether the cell is disabled, according to the calendar's `minValue`, `maxValue`, and `isDisabled` props. Disabled dates are not focusable, and cannot be selected by the user. They are typically displayed with a dimmed appearance. | | `isOutsideVisibleRange` | `[data-outside-visible-range]` | Whether the cell is outside the visible range of the calendar. For example, dates before the first day of a month in the same week. | | `isOutsideMonth` | `[data-outside-month]` | Whether the cell is outside the current month. | | `isUnavailable` | `[data-unavailable]` | Whether the cell is unavailable, according to the calendar's `isDateUnavailable` prop. Unavailable dates remain focusable, but cannot be selected by the user. They should be displayed with a visual affordance to indicate they are unavailable, such as a different color or a strikethrough. Note that because they are focusable, unavailable dates must meet a 4.5:1 color contrast ratio, as defined by WCAG. | | `isInvalid` | `[data-invalid]` | Whether the cell is part of an invalid selection. | ### Text# The help text elements within a `DateRangePicker` can be targeted with the `[slot=description]` and `[slot=errorMessage]` CSS selectors, or by adding a custom `className`. ## Advanced customization# * * * ### Composition# If you need to customize one of the components within a `DateRangePicker`, such as `Label` or `DateSegment`, in many cases you can create a wrapper component. This lets you customize the props passed to the component. function MyDateSegment(props) { return <MyDateSegment {...props} className="my-date-segment" /> } function MyDateSegment(props) { return ( <MyDateSegment {...props} className="my-date-segment" /> ); } function MyDateSegment( props ) { return ( <MyDateSegment {...props} className="my-date-segment" /> ); } ### Contexts# All React Aria Components export a corresponding context that can be used to send props to them from a parent element. This enables you to build your own compositional APIs similar to those found in React Aria Components itself. You can send any prop or ref via context that you could pass to the corresponding component. The local props and ref on the component are merged with the ones passed via context, with the local props taking precedence (following the rules documented in mergeProps). | Component | Context | Props | Ref | | --- | --- | --- | --- | | `DateRangePicker` | `DateRangePickerContext` | ` DateRangePickerProps ` | `HTMLDivElement` | This example shows a `FieldGroup` component that renders a group of date pickers with a title and optional error message. It uses the useId hook to generate a unique id for the error message. All of the child DateRangePickers are marked invalid and associated with the error message via the `aria-describedby` attribute passed to the `DateRangePickerContext` provider. import {DateRangePickerContext} from 'react-aria-components'; import {useId} from 'react-aria'; interface FieldGroupProps { title?: string; children?: React.ReactNode; errorMessage?: string; } function FieldGroup({ title, children, errorMessage }: FieldGroupProps) { let errorId = useId(); return ( <fieldset> <legend>{title}</legend> <DateRangePickerContext.Provider value={{ isInvalid: !!errorMessage, 'aria-describedby': errorMessage ? errorId : undefined }} > {children} </DateRangePickerContext.Provider> {errorMessage && ( <small id={errorId} className="invalid">{errorMessage}</small> )} </fieldset> ); } <FieldGroup title="Dates" errorMessage="Ticket sale and event dates cannot overlap." > <MyDateRangePicker label="Ticket sale dates" defaultValue={{ start: parseDate('2023-07-12'), end: parseDate('2023-08-04') }} /> <MyDateRangePicker label="Event dates" defaultValue={{ start: parseDate('2023-08-01'), end: parseDate('2023-08-10') }} /> </FieldGroup> import {DateRangePickerContext} from 'react-aria-components'; import {useId} from 'react-aria'; interface FieldGroupProps { title?: string; children?: React.ReactNode; errorMessage?: string; } function FieldGroup( { title, children, errorMessage }: FieldGroupProps ) { let errorId = useId(); return ( <fieldset> <legend>{title}</legend> <DateRangePickerContext.Provider value={{ isInvalid: !!errorMessage, 'aria-describedby': errorMessage ? errorId : undefined }} > {children} </DateRangePickerContext.Provider> {errorMessage && ( <small id={errorId} className="invalid"> {errorMessage} </small> )} </fieldset> ); } <FieldGroup title="Dates" errorMessage="Ticket sale and event dates cannot overlap." > <MyDateRangePicker label="Ticket sale dates" defaultValue={{ start: parseDate('2023-07-12'), end: parseDate('2023-08-04') }} /> <MyDateRangePicker label="Event dates" defaultValue={{ start: parseDate('2023-08-01'), end: parseDate('2023-08-10') }} /> </FieldGroup> import {DateRangePickerContext} from 'react-aria-components'; import {useId} from 'react-aria'; interface FieldGroupProps { title?: string; children?: React.ReactNode; errorMessage?: string; } function FieldGroup( { title, children, errorMessage }: FieldGroupProps ) { let errorId = useId(); return ( <fieldset> <legend> {title} </legend> <DateRangePickerContext.Provider value={{ isInvalid: !!errorMessage, 'aria-describedby': errorMessage ? errorId : undefined }} > {children} </DateRangePickerContext.Provider> {errorMessage && ( <small id={errorId} className="invalid" > {errorMessage} </small> )} </fieldset> ); } <FieldGroup title="Dates" errorMessage="Ticket sale and event dates cannot overlap." > <MyDateRangePicker label="Ticket sale dates" defaultValue={{ start: parseDate( '2023-07-12' ), end: parseDate( '2023-08-04' ) }} /> <MyDateRangePicker label="Event dates" defaultValue={{ start: parseDate( '2023-08-01' ), end: parseDate( '2023-08-10' ) }} /> </FieldGroup> Dates Ticket sale dates 7/12/2023 – 8/4/2023 ▼ Event dates 8/1/2023 – 8/10/2023 ▼ Ticket sale and event dates cannot overlap. Show CSS fieldset { padding: 1.5em; width: fit-content; } .invalid { color: var(--invalid-color); margin-top: 1em; display: block; } fieldset { padding: 1.5em; width: fit-content; } .invalid { color: var(--invalid-color); margin-top: 1em; display: block; } fieldset { padding: 1.5em; width: fit-content; } .invalid { color: var(--invalid-color); margin-top: 1em; display: block; } ### Custom children# DateRangePicker passes props to its child components, such as the label and popover, via their associated contexts. These contexts are exported so you can also consume them in your own custom components. This enables you to reuse existing components from your app or component library together with React Aria Components. | Component | Context | Props | Ref | | --- | --- | --- | --- | | `Label` | `LabelContext` | ` LabelProps ` | `HTMLLabelElement` | | `Group` | `GroupContext` | ` GroupProps ` | `HTMLDivElement` | | `Button` | `ButtonContext` | ` ButtonProps ` | `HTMLButtonElement` | | `Popover` | `PopoverContext` | ` PopoverProps ` | `HTMLElement` | | `Dialog` | `DialogContext` | ` DialogProps ` | `HTMLElement` | | `RangeCalendar` | `RangeCalendarContext` | ` RangeCalendarProps ` | `HTMLDivElement` | | `Text` | `TextContext` | ` TextProps ` | `HTMLElement` | This example consumes from `LabelContext` in an existing styled label component to make it compatible with React Aria Components. The `useContextProps` hook merges the local props and ref with the ones provided via context by DateRangePicker. import type {LabelProps} from 'react-aria-components'; import {LabelContext, useContextProps} from 'react-aria-components'; const MyCustomLabel = React.forwardRef( (props: LabelProps, ref: React.ForwardedRef<HTMLLabelElement>) => { // Merge the local props and ref with the ones provided via context. [props, ref] = useContextProps(props, ref, LabelContext); // ... your existing Label component return <label {...props} ref={ref} />; } ); import type {LabelProps} from 'react-aria-components'; import { LabelContext, useContextProps } from 'react-aria-components'; const MyCustomLabel = React.forwardRef( ( props: LabelProps, ref: React.ForwardedRef<HTMLLabelElement> ) => { // Merge the local props and ref with the ones provided via context. [props, ref] = useContextProps( props, ref, LabelContext ); // ... your existing Label component return <label {...props} ref={ref} />; } ); import type {LabelProps} from 'react-aria-components'; import { LabelContext, useContextProps } from 'react-aria-components'; const MyCustomLabel = React.forwardRef( ( props: LabelProps, ref: React.ForwardedRef< HTMLLabelElement > ) => { // Merge the local props and ref with the ones provided via context. [props, ref] = useContextProps( props, ref, LabelContext ); // ... your existing Label component return ( <label {...props} ref={ref} /> ); } ); Now you can use `MyCustomLabel` within a `DateRangePicker`, in place of the builtin React Aria Components `Label`. <DateRangePicker> <MyCustomLabel>Name</MyCustomLabel> {/* ... */} </DateRangePicker> <DateRangePicker> <MyCustomLabel>Name</MyCustomLabel> {/* ... */} </DateRangePicker> <DateRangePicker> <MyCustomLabel> Name </MyCustomLabel> {/* ... */} </DateRangePicker> ### State# DateRangePicker provides a `DateRangePickerState` object to its children via `DateRangePickerStateContext`. This can be used to access and manipulate the date range picker's state. This example shows a `DateRangePickerClearButton` component that can be placed within a `DateRangePicker` to allow the user to clear the selected value. import {DateRangePickerStateContext} from 'react-aria-components'; function DateRangePickerClearButton() { let state = React.useContext(DateRangePickerStateContext)!; return ( <Button // Don't inherit default Button behavior from DateRangePicker. slot={null} className="clear-button" aria-label="Clear" onPress={() => state.setValue(null)} > ✕ </Button> ); } <DateRangePicker defaultValue={{ start: parseDate('2023-07-12'), end: parseDate('2023-08-04') }} > <Label>Trip dates</Label> <Group> <DateInput slot="start"> {(segment) => <DateSegment segment={segment} />} </DateInput> <span aria-hidden="true">–</span> <DateInput slot="end"> {(segment) => <DateSegment segment={segment} />} </DateInput> <DateRangePickerClearButton /> <Button>▼</Button> </Group> <Popover> <Dialog> <RangeCalendar> <header> <Button slot="previous">◀</Button> <Heading /> <Button slot="next">▶</Button> </header> <CalendarGrid> {(date) => <CalendarCell date={date} />} </CalendarGrid> </RangeCalendar> </Dialog> </Popover> </DateRangePicker> import {DateRangePickerStateContext} from 'react-aria-components'; function DateRangePickerClearButton() { let state = React.useContext( DateRangePickerStateContext )!; return ( <Button // Don't inherit default Button behavior from DateRangePicker. slot={null} className="clear-button" aria-label="Clear" onPress={() => state.setValue(null)} > ✕ </Button> ); } <DateRangePicker defaultValue={{ start: parseDate('2023-07-12'), end: parseDate('2023-08-04') }} > <Label>Trip dates</Label> <Group> <DateInput slot="start"> {(segment) => <DateSegment segment={segment} />} </DateInput> <span aria-hidden="true">–</span> <DateInput slot="end"> {(segment) => <DateSegment segment={segment} />} </DateInput> <DateRangePickerClearButton /> <Button>▼</Button> </Group> <Popover> <Dialog> <RangeCalendar> <header> <Button slot="previous">◀</Button> <Heading /> <Button slot="next">▶</Button> </header> <CalendarGrid> {(date) => <CalendarCell date={date} />} </CalendarGrid> </RangeCalendar> </Dialog> </Popover> </DateRangePicker> import {DateRangePickerStateContext} from 'react-aria-components'; function DateRangePickerClearButton() { let state = React .useContext( DateRangePickerStateContext )!; return ( <Button // Don't inherit default Button behavior from DateRangePicker. slot={null} className="clear-button" aria-label="Clear" onPress={() => state.setValue( null )} > ✕ </Button> ); } <DateRangePicker defaultValue={{ start: parseDate( '2023-07-12' ), end: parseDate( '2023-08-04' ) }} > <Label> Trip dates </Label> <Group> <DateInput slot="start"> {(segment) => ( <DateSegment segment={segment} /> )} </DateInput> <span aria-hidden="true"> – </span> <DateInput slot="end"> {(segment) => ( <DateSegment segment={segment} /> )} </DateInput> <DateRangePickerClearButton /> <Button>▼</Button> </Group> <Popover> <Dialog> <RangeCalendar> <header> <Button slot="previous"> ◀ </Button> <Heading /> <Button slot="next"> ▶ </Button> </header> <CalendarGrid> {(date) => ( <CalendarCell date={date} /> )} </CalendarGrid> </RangeCalendar> </Dialog> </Popover> </DateRangePicker> Trip dates 7/12/2023 – 8/4/2023 ✕▼ Show CSS .clear-button { width: 1.143rem; height: 1.143rem; border-radius: 1.143rem; font-size: 0.857rem; line-height: 0.857rem; vertical-align: middle; text-align: center; background: gray; color: white; border: none; padding: 0; outline: none; &[data-pressed] { background: dimgray; } &[data-focus-visible] { outline: 2px solid var(--focus-ring-color); outline-offset: 2px; } + .react-aria-Button { margin-left: 4px; } } .clear-button { width: 1.143rem; height: 1.143rem; border-radius: 1.143rem; font-size: 0.857rem; line-height: 0.857rem; vertical-align: middle; text-align: center; background: gray; color: white; border: none; padding: 0; outline: none; &[data-pressed] { background: dimgray; } &[data-focus-visible] { outline: 2px solid var(--focus-ring-color); outline-offset: 2px; } + .react-aria-Button { margin-left: 4px; } } .clear-button { width: 1.143rem; height: 1.143rem; border-radius: 1.143rem; font-size: 0.857rem; line-height: 0.857rem; vertical-align: middle; text-align: center; background: gray; color: white; border: none; padding: 0; outline: none; &[data-pressed] { background: dimgray; } &[data-focus-visible] { outline: 2px solid var(--focus-ring-color); outline-offset: 2px; } + .react-aria-Button { margin-left: 4px; } } ### Hooks# If you need to customize things even further, such as accessing internal state, intercepting events, or customizing the DOM structure, you can drop down to the lower level Hook-based API. See useDateRangePicker for more details. --- ## Page: https://react-spectrum.adobe.com/react-aria/RangeCalendar.html # RangeCalendar A range calendar displays one or more date grids and allows users to select a contiguous range of dates. <table><tbody><tr><th>install</th><td><code>yarn add react-aria-components</code></td></tr><tr><th>version</th><td>1.10.1</td></tr><tr><th>usage</th><td><code><span>import</span> {RangeCalendar} <span>from</span> <span>'react-aria-components'</span></code></td></tr></tbody></table> View repository GitHub View package NPM ## Example# * * * import {Button, CalendarCell, CalendarGrid, Heading, RangeCalendar} from 'react-aria-components'; <RangeCalendar aria-label="Trip dates"> <header> <Button slot="previous">◀</Button> <Heading /> <Button slot="next">▶</Button> </header> <CalendarGrid> {(date) => <CalendarCell date={date} />} </CalendarGrid> </RangeCalendar> import { Button, CalendarCell, CalendarGrid, Heading, RangeCalendar } from 'react-aria-components'; <RangeCalendar aria-label="Trip dates"> <header> <Button slot="previous">◀</Button> <Heading /> <Button slot="next">▶</Button> </header> <CalendarGrid> {(date) => <CalendarCell date={date} />} </CalendarGrid> </RangeCalendar> import { Button, CalendarCell, CalendarGrid, Heading, RangeCalendar } from 'react-aria-components'; <RangeCalendar aria-label="Trip dates"> <header> <Button slot="previous"> ◀ </Button> <Heading /> <Button slot="next"> ▶ </Button> </header> <CalendarGrid> {(date) => ( <CalendarCell date={date} /> )} </CalendarGrid> </RangeCalendar> ## Trip dates, June 2025 ◀ ## June 2025 ▶ | S | M | T | W | T | F | S | | --- | --- | --- | --- | --- | --- | --- | | 1 | 2 | 3 | 4 | 5 | 6 | 7 | | 8 | 9 | 10 | 11 | 12 | 13 | 14 | | 15 | 16 | 17 | 18 | 19 | 20 | 21 | | 22 | 23 | 24 | 25 | 26 | 27 | 28 | | 29 | 30 | 1 | 2 | 3 | 4 | 5 | Show CSS @import "@react-aria/example-theme"; .react-aria-RangeCalendar { width: fit-content; max-width: 100%; color: var(--text-color); & header { display: flex; align-items: center; margin: 0 4px .5rem 4px; .react-aria-Heading { flex: 1; margin: 0; text-align: center; font-size: 1.375rem; } } .react-aria-Button { width: 2rem; height: 2rem; padding: 0; } & table { border-collapse: collapse; & td { padding: 2px 0; } } .react-aria-CalendarCell { width: 2.286rem; line-height: 2.286rem; text-align: center; border-radius: 6px; cursor: default; outline: none; forced-color-adjust: none; &[data-outside-month] { display: none; } &[data-pressed] { background: var(--gray-100); } &[data-focus-visible] { outline: 2px solid var(--highlight-background); outline-offset: -2px; } &[data-selected] { background: var(--highlight-background); color: var(--highlight-foreground); border-radius: 0; &[data-focus-visible] { outline-color: var(--highlight-foreground); outline-offset: -3px; } } &[data-selection-start] { border-start-start-radius: 6px; border-end-start-radius: 6px; } &[data-selection-end] { border-start-end-radius: 6px; border-end-end-radius: 6px; } } } @import "@react-aria/example-theme"; .react-aria-RangeCalendar { width: fit-content; max-width: 100%; color: var(--text-color); & header { display: flex; align-items: center; margin: 0 4px .5rem 4px; .react-aria-Heading { flex: 1; margin: 0; text-align: center; font-size: 1.375rem; } } .react-aria-Button { width: 2rem; height: 2rem; padding: 0; } & table { border-collapse: collapse; & td { padding: 2px 0; } } .react-aria-CalendarCell { width: 2.286rem; line-height: 2.286rem; text-align: center; border-radius: 6px; cursor: default; outline: none; forced-color-adjust: none; &[data-outside-month] { display: none; } &[data-pressed] { background: var(--gray-100); } &[data-focus-visible] { outline: 2px solid var(--highlight-background); outline-offset: -2px; } &[data-selected] { background: var(--highlight-background); color: var(--highlight-foreground); border-radius: 0; &[data-focus-visible] { outline-color: var(--highlight-foreground); outline-offset: -3px; } } &[data-selection-start] { border-start-start-radius: 6px; border-end-start-radius: 6px; } &[data-selection-end] { border-start-end-radius: 6px; border-end-end-radius: 6px; } } } @import "@react-aria/example-theme"; .react-aria-RangeCalendar { width: fit-content; max-width: 100%; color: var(--text-color); & header { display: flex; align-items: center; margin: 0 4px .5rem 4px; .react-aria-Heading { flex: 1; margin: 0; text-align: center; font-size: 1.375rem; } } .react-aria-Button { width: 2rem; height: 2rem; padding: 0; } & table { border-collapse: collapse; & td { padding: 2px 0; } } .react-aria-CalendarCell { width: 2.286rem; line-height: 2.286rem; text-align: center; border-radius: 6px; cursor: default; outline: none; forced-color-adjust: none; &[data-outside-month] { display: none; } &[data-pressed] { background: var(--gray-100); } &[data-focus-visible] { outline: 2px solid var(--highlight-background); outline-offset: -2px; } &[data-selected] { background: var(--highlight-background); color: var(--highlight-foreground); border-radius: 0; &[data-focus-visible] { outline-color: var(--highlight-foreground); outline-offset: -3px; } } &[data-selection-start] { border-start-start-radius: 6px; border-end-start-radius: 6px; } &[data-selection-end] { border-start-end-radius: 6px; border-end-end-radius: 6px; } } } ## Features# * * * There is no standalone range calendar element in HTML. Two separate `<input type="date">` elements could be used, but this is very limited in functionality, lacking in internationalization capabilities, inconsistent between browsers, and difficult to style. `RangeCalendar` helps achieve accessible and international range calendar components that can be styled as needed. * **Flexible** – Display one or more months at once, or a custom time range for use cases like a week view. Minimum and maximum values, unavailable dates, and non-contiguous selections are supported as well. * **International** – Support for 13 calendar systems used around the world, including Gregorian, Buddhist, Islamic, Persian, and more. Locale-specific formatting, number systems, and right-to-left support are available as well. * **Accessible** – Calendar cells can be navigated and selected using the keyboard, and localized screen reader messages are included to announce when the selection and visible date range change. * **Touch friendly** – Date ranges can be selected by dragging over dates in the calendar using a touch screen, and all interactions are accessible using touch-based screen readers. * **Customizable** – As with all of React Aria, the DOM structure and styling of all elements can be fully customized. Read our blog post for more details about the internationalization, accessibility, and user experience features implemented by `RangeCalendar`. ## Anatomy# * * * A range calendar consists of a grouping element containing one or more date grids (e.g. months), and a previous and next button for navigating through time. Each calendar grid consists of cells containing button elements that can be pressed and navigated to using the arrow keys to select a date range. Once a start date is selected, the user can navigate to another date using the keyboard or by hovering over it, and clicking it or pressing the Enter key commits the selected date range. `RangeCalendar` also supports an optional error message element, which can be used to provide more context about any validation errors. This is linked with the calendar via the `aria-describedby` attribute. import {Button, CalendarCell, CalendarGrid, CalendarGridBody, CalendarGridHeader, CalendarHeaderCell, Heading, RangeCalendar, Text} from 'react-aria-components'; <RangeCalendar> <Button slot="previous" /> <Heading /> <Button slot="next" /> <CalendarGrid> <CalendarGridHeader> {(day) => <CalendarHeaderCell />} </CalendarGridHeader> <CalendarGridBody> {(date) => <CalendarCell date={date} />} </CalendarGridBody> </CalendarGrid> <Text slot="errorMessage" /> </RangeCalendar> import { Button, CalendarCell, CalendarGrid, CalendarGridBody, CalendarGridHeader, CalendarHeaderCell, Heading, RangeCalendar, Text } from 'react-aria-components'; <RangeCalendar> <Button slot="previous" /> <Heading /> <Button slot="next" /> <CalendarGrid> <CalendarGridHeader> {(day) => <CalendarHeaderCell />} </CalendarGridHeader> <CalendarGridBody> {(date) => <CalendarCell date={date} />} </CalendarGridBody> </CalendarGrid> <Text slot="errorMessage" /> </RangeCalendar> import { Button, CalendarCell, CalendarGrid, CalendarGridBody, CalendarGridHeader, CalendarHeaderCell, Heading, RangeCalendar, Text } from 'react-aria-components'; <RangeCalendar> <Button slot="previous" /> <Heading /> <Button slot="next" /> <CalendarGrid> <CalendarGridHeader> {(day) => ( <CalendarHeaderCell /> )} </CalendarGridHeader> <CalendarGridBody> {(date) => ( <CalendarCell date={date} /> )} </CalendarGridBody> </CalendarGrid> <Text slot="errorMessage" /> </RangeCalendar> Note that much of this anatomy is shared with non-range calendars. If you have both, the styling and internal components such as `CalendarCell` can be shared. ### Concepts# `RangeCalendar` makes use of the following concepts: @internationalized/date Represent and manipulate dates and times in a locale-aware manner. ### Composed components# A `Calendar` uses the following components, which may also be used standalone or reused in other components. Button A button allows a user to perform an action. ## Starter kits# * * * To help kick-start your project, we offer starter kits that include example implementations of all React Aria components with various styling solutions. All components are fully styled, including support for dark mode, high contrast mode, and all UI states. Each starter comes with a pre-configured Storybook that you can experiment with, or use as a starting point for your own component library. Vanilla CSS Download ZIP Preview Tailwind CSS Download ZIP Preview ## Reusable wrappers# * * * If you will use a RangeCalendar in multiple places in your app, you can wrap all of the pieces into a reusable component. This way, the DOM structure, styling code, and other logic are defined in a single place and reused everywhere to ensure consistency. import type {DateValue, RangeCalendarProps} from 'react-aria-components'; import {Text} from 'react-aria-components'; interface MyRangeCalendarProps<T extends DateValue> extends RangeCalendarProps<T> { errorMessage?: string; } function MyRangeCalendar<T extends DateValue>( { errorMessage, ...props }: MyRangeCalendarProps<T> ) { return ( <RangeCalendar {...props}> <header> <Button slot="previous">◀</Button> <Heading /> <Button slot="next">▶</Button> </header> <CalendarGrid> {(date) => <CalendarCell date={date} />} </CalendarGrid> {errorMessage && <Text slot="errorMessage">{errorMessage}</Text>} </RangeCalendar> ); } <MyRangeCalendar aria-label="Trip dates" /> import type { DateValue, RangeCalendarProps } from 'react-aria-components'; import {Text} from 'react-aria-components'; interface MyRangeCalendarProps<T extends DateValue> extends RangeCalendarProps<T> { errorMessage?: string; } function MyRangeCalendar<T extends DateValue>( { errorMessage, ...props }: MyRangeCalendarProps<T> ) { return ( <RangeCalendar {...props}> <header> <Button slot="previous">◀</Button> <Heading /> <Button slot="next">▶</Button> </header> <CalendarGrid> {(date) => <CalendarCell date={date} />} </CalendarGrid> {errorMessage && ( <Text slot="errorMessage">{errorMessage}</Text> )} </RangeCalendar> ); } <MyRangeCalendar aria-label="Trip dates" /> import type { DateValue, RangeCalendarProps } from 'react-aria-components'; import {Text} from 'react-aria-components'; interface MyRangeCalendarProps< T extends DateValue > extends RangeCalendarProps<T> { errorMessage?: string; } function MyRangeCalendar< T extends DateValue >( { errorMessage, ...props }: MyRangeCalendarProps< T > ) { return ( <RangeCalendar {...props} > <header> <Button slot="previous"> ◀ </Button> <Heading /> <Button slot="next"> ▶ </Button> </header> <CalendarGrid> {(date) => ( <CalendarCell date={date} /> )} </CalendarGrid> {errorMessage && ( <Text slot="errorMessage"> {errorMessage} </Text> )} </RangeCalendar> ); } <MyRangeCalendar aria-label="Trip dates" /> ## Trip dates, June 2025 ◀ ## June 2025 ▶ | S | M | T | W | T | F | S | | --- | --- | --- | --- | --- | --- | --- | | 1 | 2 | 3 | 4 | 5 | 6 | 7 | | 8 | 9 | 10 | 11 | 12 | 13 | 14 | | 15 | 16 | 17 | 18 | 19 | 20 | 21 | | 22 | 23 | 24 | 25 | 26 | 27 | 28 | | 29 | 30 | 1 | 2 | 3 | 4 | 5 | ## Value# * * * A `RangeCalendar` has no selection by default. An initial, uncontrolled value can be provided to the `RangeCalendar` using the `defaultValue` prop. Alternatively, a controlled value can be provided using the `value` prop. Date ranges are objects with `start` and `end` properties containing date values, which are provided using objects in the @internationalized/date package. This library handles correct international date manipulation across calendars, time zones, and other localization concerns. `RangeCalendar` supports values with both date and time components, but only allows users to modify the dates. By default, `RangeCalendar` will emit `CalendarDate` objects in the `onChange` event, but if a` CalendarDateTime `or` ZonedDateTime `object is passed as the `value` or `defaultValue`, values of that type will be emitted, changing only the date and preserving the time components. import {parseDate} from '@internationalized/date'; function Example() { let [value, setValue] = React.useState({ start: parseDate('2020-02-03'), end: parseDate('2020-02-12') }); return ( <div style={{display: 'flex', gap: 20, flexWrap: 'wrap'}}> <MyRangeCalendar aria-label="Date range (uncontrolled)" defaultValue={{ start: parseDate('2020-02-03'), end: parseDate('2020-02-12') }} /> <MyRangeCalendar aria-label="Date range (controlled)" value={value} onChange={setValue} /> </div> ); } import {parseDate} from '@internationalized/date'; function Example() { let [value, setValue] = React.useState({ start: parseDate('2020-02-03'), end: parseDate('2020-02-12') }); return ( <div style={{ display: 'flex', gap: 20, flexWrap: 'wrap' }} > <MyRangeCalendar aria-label="Date range (uncontrolled)" defaultValue={{ start: parseDate('2020-02-03'), end: parseDate('2020-02-12') }} /> <MyRangeCalendar aria-label="Date range (controlled)" value={value} onChange={setValue} /> </div> ); } import {parseDate} from '@internationalized/date'; function Example() { let [value, setValue] = React.useState({ start: parseDate( '2020-02-03' ), end: parseDate( '2020-02-12' ) }); return ( <div style={{ display: 'flex', gap: 20, flexWrap: 'wrap' }} > <MyRangeCalendar aria-label="Date range (uncontrolled)" defaultValue={{ start: parseDate( '2020-02-03' ), end: parseDate( '2020-02-12' ) }} /> <MyRangeCalendar aria-label="Date range (controlled)" value={value} onChange={setValue} /> </div> ); } ## Date range (uncontrolled), February 2020 ◀ ## February 2020 ▶ | S | M | T | W | T | F | S | | --- | --- | --- | --- | --- | --- | --- | | 26 | 27 | 28 | 29 | 30 | 31 | 1 | | 2 | 3 | 4 | 5 | 6 | 7 | 8 | | 9 | 10 | 11 | 12 | 13 | 14 | 15 | | 16 | 17 | 18 | 19 | 20 | 21 | 22 | | 23 | 24 | 25 | 26 | 27 | 28 | 29 | ## Date range (controlled), February 2020 ◀ ## February 2020 ▶ | S | M | T | W | T | F | S | | --- | --- | --- | --- | --- | --- | --- | | 26 | 27 | 28 | 29 | 30 | 31 | 1 | | 2 | 3 | 4 | 5 | 6 | 7 | 8 | | 9 | 10 | 11 | 12 | 13 | 14 | 15 | | 16 | 17 | 18 | 19 | 20 | 21 | 22 | | 23 | 24 | 25 | 26 | 27 | 28 | 29 | ### International calendars# `RangeCalendar` supports selecting dates in many calendar systems used around the world, including Gregorian, Hebrew, Indian, Islamic, Buddhist, and more. Dates are automatically displayed in the appropriate calendar system for the user's locale. The calendar system can be overridden using the Unicode calendar locale extension, passed to the I18nProvider component. Selected dates passed to `onChange` always use the same calendar system as the `value` or `defaultValue` prop. If no `value` or `defaultValue` is provided, then dates passed to `onChange` are always in the Gregorian calendar since this is the most commonly used. This means that even though the user selects dates in their local calendar system, applications are able to deal with dates from all users consistently. The below example displays a `RangeCalendar` in the Hindi language, using the Indian calendar. Dates emitted from `onChange` are in the Gregorian calendar. import {I18nProvider} from 'react-aria'; import type {DateRange} from 'react-aria-components'; function Example() { let [range, setRange] = React.useState<DateRange | null>(null); return ( <I18nProvider locale="hi-IN-u-ca-indian"> <MyRangeCalendar aria-label="Date range" value={range} onChange={setRange} /> <p>Start date: {range?.start.toString()}</p> <p>End date: {range?.end.toString()}</p> </I18nProvider> ); } import {I18nProvider} from 'react-aria'; import type {DateRange} from 'react-aria-components'; function Example() { let [range, setRange] = React.useState<DateRange | null>( null ); return ( <I18nProvider locale="hi-IN-u-ca-indian"> <MyRangeCalendar aria-label="Date range" value={range} onChange={setRange} /> <p>Start date: {range?.start.toString()}</p> <p>End date: {range?.end.toString()}</p> </I18nProvider> ); } import {I18nProvider} from 'react-aria'; import type {DateRange} from 'react-aria-components'; function Example() { let [range, setRange] = React.useState< DateRange | null >(null); return ( <I18nProvider locale="hi-IN-u-ca-indian"> <MyRangeCalendar aria-label="Date range" value={range} onChange={setRange} /> <p> Start date:{' '} {range?.start .toString()} </p> <p> End date:{' '} {range?.end .toString()} </p> </I18nProvider> ); } ## Date range, शक 1947 ज्येष्ठ ◀ ## शक 1947 ज्येष्ठ ▶ | र | सो | मं | बु | गु | शु | श | | --- | --- | --- | --- | --- | --- | --- | | 28 | 29 | 30 | 31 | 1 | 2 | 3 | | 4 | 5 | 6 | 7 | 8 | 9 | 10 | | 11 | 12 | 13 | 14 | 15 | 16 | 17 | | 18 | 19 | 20 | 21 | 22 | 23 | 24 | | 25 | 26 | 27 | 28 | 29 | 30 | 31 | Start date: End date: ### Custom calendar systems# `RangeCalendar` also supports custom calendar systems that implement custom business rules. An example would be a fiscal year calendar that follows a 4-5-4 format, where month ranges don't follow the usual Gregorian calendar. The `createCalendar` prop accepts a function that returns an instance of the `Calendar` interface. See the @internationalized/date docs for an example implementation. import {GregorianCalendar} from '@internationalized/date'; function Example() { return ( <MyRangeCalendar firstDayOfWeek="sun" createCalendar={() => new Custom454()} /> ); } class Custom454 extends GregorianCalendar { // See @internationalized/date docs linked above... } import {GregorianCalendar} from '@internationalized/date'; function Example() { return ( <MyRangeCalendar firstDayOfWeek="sun" createCalendar={() => new Custom454()} /> ); } class Custom454 extends GregorianCalendar { // See @internationalized/date docs linked above... } import {GregorianCalendar} from '@internationalized/date'; function Example() { return ( <MyRangeCalendar firstDayOfWeek="sun" createCalendar={() => new Custom454()} /> ); } class Custom454 extends GregorianCalendar { // See @internationalized/date docs linked above... } ## June 2025 ◀ ## June 2025 ▶ | S | M | T | W | T | F | S | | --- | --- | --- | --- | --- | --- | --- | | 1 | 2 | 3 | 4 | 5 | 6 | 7 | | 8 | 9 | 10 | 11 | 12 | 13 | 14 | | 15 | 16 | 17 | 18 | 19 | 20 | 21 | | 22 | 23 | 24 | 25 | 26 | 27 | 28 | ## Events# * * * `RangeCalendar` accepts an `onChange` prop which is triggered whenever a date is selected by the user. The example below uses `onChange` to update a separate element with a formatted version of the date in the user's locale. This is done by converting the date to a native JavaScript `Date` object to pass to the formatter. import {useDateFormatter} from 'react-aria'; import {getLocalTimeZone} from '@internationalized/date'; function Example() { let [range, setRange] = React.useState({ start: parseDate('2020-07-03'), end: parseDate('2020-07-10') }); let formatter = useDateFormatter({ dateStyle: 'long' }); return ( <> <MyRangeCalendar aria-label="Date range" value={range} onChange={setRange} /> <p> Selected date: {formatter.formatRange( range.start.toDate(getLocalTimeZone()), range.end.toDate(getLocalTimeZone()) )} </p> </> ); } import {useDateFormatter} from 'react-aria'; import {getLocalTimeZone} from '@internationalized/date'; function Example() { let [range, setRange] = React.useState({ start: parseDate('2020-07-03'), end: parseDate('2020-07-10') }); let formatter = useDateFormatter({ dateStyle: 'long' }); return ( <> <MyRangeCalendar aria-label="Date range" value={range} onChange={setRange} /> <p> Selected date: {formatter.formatRange( range.start.toDate(getLocalTimeZone()), range.end.toDate(getLocalTimeZone()) )} </p> </> ); } import {useDateFormatter} from 'react-aria'; import {getLocalTimeZone} from '@internationalized/date'; function Example() { let [range, setRange] = React.useState({ start: parseDate( '2020-07-03' ), end: parseDate( '2020-07-10' ) }); let formatter = useDateFormatter({ dateStyle: 'long' }); return ( <> <MyRangeCalendar aria-label="Date range" value={range} onChange={setRange} /> <p> Selected date: {' '} {formatter .formatRange( range.start .toDate( getLocalTimeZone() ), range.end .toDate( getLocalTimeZone() ) )} </p> </> ); } ## Date range, July 2020 ◀ ## July 2020 ▶ | S | M | T | W | T | F | S | | --- | --- | --- | --- | --- | --- | --- | | 28 | 29 | 30 | 1 | 2 | 3 | 4 | | 5 | 6 | 7 | 8 | 9 | 10 | 11 | | 12 | 13 | 14 | 15 | 16 | 17 | 18 | | 19 | 20 | 21 | 22 | 23 | 24 | 25 | | 26 | 27 | 28 | 29 | 30 | 31 | 1 | Selected date: July 3 – 10, 2020 ## Multi-month# * * * Multiple `CalendarGrid` elements can be rendered to show multiple months at once. The `visibleDuration` prop should be provided to the `RangeCalendar` component to specify how many months are visible, and each `CalendarGrid` accepts an `offset` prop to specify its starting date relative to the first visible date. <RangeCalendar aria-label="Trip dates" visibleDuration={{months: 3}}> <header> <Button slot="previous">◀</Button> <Heading /> <Button slot="next">▶</Button> </header> <div style={{display: 'flex', gap: 30, overflow: 'auto'}}> <CalendarGrid> {date => <CalendarCell date={date} />} </CalendarGrid> <CalendarGrid offset={{months: 1}}> {date => <CalendarCell date={date} />} </CalendarGrid> <CalendarGrid offset={{months: 2}}> {date => <CalendarCell date={date} />} </CalendarGrid> </div> </RangeCalendar> <RangeCalendar aria-label="Trip dates" visibleDuration={{ months: 3 }} > <header> <Button slot="previous">◀</Button> <Heading /> <Button slot="next">▶</Button> </header> <div style={{ display: 'flex', gap: 30, overflow: 'auto' }} > <CalendarGrid> {(date) => <CalendarCell date={date} />} </CalendarGrid> <CalendarGrid offset={{ months: 1 }}> {(date) => <CalendarCell date={date} />} </CalendarGrid> <CalendarGrid offset={{ months: 2 }}> {(date) => <CalendarCell date={date} />} </CalendarGrid> </div> </RangeCalendar> <RangeCalendar aria-label="Trip dates" visibleDuration={{ months: 3 }} > <header> <Button slot="previous"> ◀ </Button> <Heading /> <Button slot="next"> ▶ </Button> </header> <div style={{ display: 'flex', gap: 30, overflow: 'auto' }} > <CalendarGrid> {(date) => ( <CalendarCell date={date} /> )} </CalendarGrid> <CalendarGrid offset={{ months: 1 }} > {(date) => ( <CalendarCell date={date} /> )} </CalendarGrid> <CalendarGrid offset={{ months: 2 }} > {(date) => ( <CalendarCell date={date} /> )} </CalendarGrid> </div> </RangeCalendar> ## Trip dates, May to July 2025 ◀ ## May – July 2025 ▶ | S | M | T | W | T | F | S | | --- | --- | --- | --- | --- | --- | --- | | 27 | 28 | 29 | 30 | 1 | 2 | 3 | | 4 | 5 | 6 | 7 | 8 | 9 | 10 | | 11 | 12 | 13 | 14 | 15 | 16 | 17 | | 18 | 19 | 20 | 21 | 22 | 23 | 24 | | 25 | 26 | 27 | 28 | 29 | 30 | 31 | | S | M | T | W | T | F | S | | --- | --- | --- | --- | --- | --- | --- | | 1 | 2 | 3 | 4 | 5 | 6 | 7 | | 8 | 9 | 10 | 11 | 12 | 13 | 14 | | 15 | 16 | 17 | 18 | 19 | 20 | 21 | | 22 | 23 | 24 | 25 | 26 | 27 | 28 | | 29 | 30 | 1 | 2 | 3 | 4 | 5 | | S | M | T | W | T | F | S | | --- | --- | --- | --- | --- | --- | --- | | 29 | 30 | 1 | 2 | 3 | 4 | 5 | | 6 | 7 | 8 | 9 | 10 | 11 | 12 | | 13 | 14 | 15 | 16 | 17 | 18 | 19 | | 20 | 21 | 22 | 23 | 24 | 25 | 26 | | 27 | 28 | 29 | 30 | 31 | 1 | 2 | ### Page behavior# The `pageBehavior` prop allows you to control how the calendar navigates between months. By default, the calendar will navigate by `visibleDuration`, but by setting `pageBehavior` to `single`, pagination will be by one month. <RangeCalendar aria-label="Trip dates" visibleDuration={{ months: 3 }} pageBehavior="single" > <header> <Button slot="previous">◀</Button> <Heading /> <Button slot="next">▶</Button> </header> <div style={{ display: 'flex', gap: 30, overflow: 'auto' }}> <CalendarGrid> {(date) => <CalendarCell date={date} />} </CalendarGrid> <CalendarGrid offset={{ months: 1 }}> {(date) => <CalendarCell date={date} />} </CalendarGrid> <CalendarGrid offset={{ months: 2 }}> {(date) => <CalendarCell date={date} />} </CalendarGrid> </div> </RangeCalendar> <RangeCalendar aria-label="Trip dates" visibleDuration={{ months: 3 }} pageBehavior="single" > <header> <Button slot="previous">◀</Button> <Heading /> <Button slot="next">▶</Button> </header> <div style={{ display: 'flex', gap: 30, overflow: 'auto' }} > <CalendarGrid> {(date) => <CalendarCell date={date} />} </CalendarGrid> <CalendarGrid offset={{ months: 1 }}> {(date) => <CalendarCell date={date} />} </CalendarGrid> <CalendarGrid offset={{ months: 2 }}> {(date) => <CalendarCell date={date} />} </CalendarGrid> </div> </RangeCalendar> <RangeCalendar aria-label="Trip dates" visibleDuration={{ months: 3 }} pageBehavior="single" > <header> <Button slot="previous"> ◀ </Button> <Heading /> <Button slot="next"> ▶ </Button> </header> <div style={{ display: 'flex', gap: 30, overflow: 'auto' }} > <CalendarGrid> {(date) => ( <CalendarCell date={date} /> )} </CalendarGrid> <CalendarGrid offset={{ months: 1 }} > {(date) => ( <CalendarCell date={date} /> )} </CalendarGrid> <CalendarGrid offset={{ months: 2 }} > {(date) => ( <CalendarCell date={date} /> )} </CalendarGrid> </div> </RangeCalendar> ## Trip dates, May to July 2025 ◀ ## May – July 2025 ▶ | S | M | T | W | T | F | S | | --- | --- | --- | --- | --- | --- | --- | | 27 | 28 | 29 | 30 | 1 | 2 | 3 | | 4 | 5 | 6 | 7 | 8 | 9 | 10 | | 11 | 12 | 13 | 14 | 15 | 16 | 17 | | 18 | 19 | 20 | 21 | 22 | 23 | 24 | | 25 | 26 | 27 | 28 | 29 | 30 | 31 | | S | M | T | W | T | F | S | | --- | --- | --- | --- | --- | --- | --- | | 1 | 2 | 3 | 4 | 5 | 6 | 7 | | 8 | 9 | 10 | 11 | 12 | 13 | 14 | | 15 | 16 | 17 | 18 | 19 | 20 | 21 | | 22 | 23 | 24 | 25 | 26 | 27 | 28 | | 29 | 30 | 1 | 2 | 3 | 4 | 5 | | S | M | T | W | T | F | S | | --- | --- | --- | --- | --- | --- | --- | | 29 | 30 | 1 | 2 | 3 | 4 | 5 | | 6 | 7 | 8 | 9 | 10 | 11 | 12 | | 13 | 14 | 15 | 16 | 17 | 18 | 19 | | 20 | 21 | 22 | 23 | 24 | 25 | 26 | | 27 | 28 | 29 | 30 | 31 | 1 | 2 | ## Validation# * * * By default, `RangeCalendar` allows selecting any date range. The `minValue` and `maxValue` props can also be used to prevent the user from selecting dates outside a certain range. This example only accepts dates after today. import {today} from '@internationalized/date'; <MyRangeCalendar aria-label="Trip dates" minValue={today(getLocalTimeZone())} /> import {today} from '@internationalized/date'; <MyRangeCalendar aria-label="Trip dates" minValue={today(getLocalTimeZone())} /> import {today} from '@internationalized/date'; <MyRangeCalendar aria-label="Trip dates" minValue={today( getLocalTimeZone() )} /> ## Trip dates, June 2025 ◀ ## June 2025 ▶ | S | M | T | W | T | F | S | | --- | --- | --- | --- | --- | --- | --- | | 1 | 2 | 3 | 4 | 5 | 6 | 7 | | 8 | 9 | 10 | 11 | 12 | 13 | 14 | | 15 | 16 | 17 | 18 | 19 | 20 | 21 | | 22 | 23 | 24 | 25 | 26 | 27 | 28 | | 29 | 30 | 1 | 2 | 3 | 4 | 5 | Show CSS .react-aria-RangeCalendar { .react-aria-CalendarCell { &[data-disabled] { color: var(--text-color-disabled); } } } .react-aria-RangeCalendar { .react-aria-CalendarCell { &[data-disabled] { color: var(--text-color-disabled); } } } .react-aria-RangeCalendar { .react-aria-CalendarCell { &[data-disabled] { color: var(--text-color-disabled); } } } ### Unavailable dates# `RangeCalendar` supports marking certain dates as _unavailable_. These dates remain focusable with the keyboard so that navigation is consistent, but cannot be selected by the user. In this example, they are displayed in red. The `isDateUnavailable` prop accepts a callback that is called to evaluate whether each visible date is unavailable. Note that by default, users may not select non-contiguous ranges, i.e. ranges that contain unavailable dates within them. Once a start date is selected, enabled dates will be restricted to subsequent dates until an unavailable date is hit. See below for an example of how to allow non-contiguous ranges. This example includes multiple unavailable date ranges, e.g. dates when a rental house is not available. The `minValue` prop is also used to prevent selecting dates before today. import {today} from '@internationalized/date'; function Example() { let now = today(getLocalTimeZone()); let disabledRanges = [ [now, now.add({ days: 5 })], [now.add({ days: 14 }), now.add({ days: 16 })], [now.add({ days: 23 }), now.add({ days: 24 })] ]; let isDateUnavailable = (date: DateValue) => disabledRanges.some((interval) => date.compare(interval[0]) >= 0 && date.compare(interval[1]) <= 0 ); return ( <MyRangeCalendar aria-label="Trip dates" minValue={today(getLocalTimeZone())} isDateUnavailable={isDateUnavailable} /> ); } import {today} from '@internationalized/date'; function Example() { let now = today(getLocalTimeZone()); let disabledRanges = [ [now, now.add({ days: 5 })], [now.add({ days: 14 }), now.add({ days: 16 })], [now.add({ days: 23 }), now.add({ days: 24 })] ]; let isDateUnavailable = (date: DateValue) => disabledRanges.some((interval) => date.compare(interval[0]) >= 0 && date.compare(interval[1]) <= 0 ); return ( <MyRangeCalendar aria-label="Trip dates" minValue={today(getLocalTimeZone())} isDateUnavailable={isDateUnavailable} /> ); } import {today} from '@internationalized/date'; function Example() { let now = today( getLocalTimeZone() ); let disabledRanges = [ [ now, now.add({ days: 5 }) ], [ now.add({ days: 14 }), now.add({ days: 16 }) ], [ now.add({ days: 23 }), now.add({ days: 24 }) ] ]; let isDateUnavailable = (date: DateValue) => disabledRanges .some(( interval ) => date.compare( interval[0] ) >= 0 && date.compare( interval[1] ) <= 0 ); return ( <MyRangeCalendar aria-label="Trip dates" minValue={today( getLocalTimeZone() )} isDateUnavailable={isDateUnavailable} /> ); } ## Trip dates, June 2025 ◀ ## June 2025 ▶ | S | M | T | W | T | F | S | | --- | --- | --- | --- | --- | --- | --- | | 1 | 2 | 3 | 4 | 5 | 6 | 7 | | 8 | 9 | 10 | 11 | 12 | 13 | 14 | | 15 | 16 | 17 | 18 | 19 | 20 | 21 | | 22 | 23 | 24 | 25 | 26 | 27 | 28 | | 29 | 30 | 1 | 2 | 3 | 4 | 5 | Show CSS .react-aria-RangeCalendar { .react-aria-CalendarCell { &[data-unavailable] { text-decoration: line-through; color: var(--invalid-color); } &[data-invalid] { background: var(--invalid-color); color: var(--highlight-foreground); } } } .react-aria-RangeCalendar { .react-aria-CalendarCell { &[data-unavailable] { text-decoration: line-through; color: var(--invalid-color); } &[data-invalid] { background: var(--invalid-color); color: var(--highlight-foreground); } } } .react-aria-RangeCalendar { .react-aria-CalendarCell { &[data-unavailable] { text-decoration: line-through; color: var(--invalid-color); } &[data-invalid] { background: var(--invalid-color); color: var(--highlight-foreground); } } } ### Non-contiguous ranges# The `allowsNonContiguousRanges` prop enables a range to be selected even if there are unavailable dates in the middle. The value emitted in the `onChange` event will still be a single range with a `start` and `end` property, but unavailable dates will not be displayed as selected. It is up to applications to split the full selected range into multiple as needed for business logic. This example prevents selecting weekends, but allows selecting ranges that span multiple weeks. import {useLocale} from 'react-aria'; import {isWeekend} from '@internationalized/date'; function Example() { let { locale } = useLocale(); return ( <MyRangeCalendar aria-label="Time off request" isDateUnavailable={(date) => isWeekend(date, locale)} allowsNonContiguousRanges /> ); } import {useLocale} from 'react-aria'; import {isWeekend} from '@internationalized/date'; function Example() { let { locale } = useLocale(); return ( <MyRangeCalendar aria-label="Time off request" isDateUnavailable={(date) => isWeekend(date, locale)} allowsNonContiguousRanges /> ); } import {useLocale} from 'react-aria'; import {isWeekend} from '@internationalized/date'; function Example() { let { locale } = useLocale(); return ( <MyRangeCalendar aria-label="Time off request" isDateUnavailable={(date) => isWeekend( date, locale )} allowsNonContiguousRanges /> ); } ## Time off request, June 2025 ◀ ## June 2025 ▶ | S | M | T | W | T | F | S | | --- | --- | --- | --- | --- | --- | --- | | 1 | 2 | 3 | 4 | 5 | 6 | 7 | | 8 | 9 | 10 | 11 | 12 | 13 | 14 | | 15 | 16 | 17 | 18 | 19 | 20 | 21 | | 22 | 23 | 24 | 25 | 26 | 27 | 28 | | 29 | 30 | 1 | 2 | 3 | 4 | 5 | ### Error message# `RangeCalendar` tries to avoid allowing the user to select invalid dates in the first place (see Validation and Unavailable dates above). However, if according to application logic a selected date range is invalid, the `isInvalid` prop can be set. This alerts assistive technology users that the selection is invalid, and can be used for styling purposes as well. In addition, the `errorMessage` slot may be used to help the user fix the issue. This example validates that the selected date range is a maximum of 1 week in duration. import {today} from '@internationalized/date'; function Example() { let [range, setRange] = React.useState({ start: today(getLocalTimeZone()), end: today(getLocalTimeZone()).add({ weeks: 1, days: 3 }) }); let isInvalid = range.end.compare(range.start) > 7; return ( <MyRangeCalendar aria-label="Trip dates" value={range} onChange={setRange} isInvalid={isInvalid} errorMessage={isInvalid ? 'Maximum stay duration is 1 week' : undefined} /> ); } import {today} from '@internationalized/date'; function Example() { let [range, setRange] = React.useState({ start: today(getLocalTimeZone()), end: today(getLocalTimeZone()).add({ weeks: 1, days: 3 }) }); let isInvalid = range.end.compare(range.start) > 7; return ( <MyRangeCalendar aria-label="Trip dates" value={range} onChange={setRange} isInvalid={isInvalid} errorMessage={isInvalid ? 'Maximum stay duration is 1 week' : undefined} /> ); } import {today} from '@internationalized/date'; function Example() { let [range, setRange] = React.useState({ start: today( getLocalTimeZone() ), end: today( getLocalTimeZone() ).add({ weeks: 1, days: 3 }) }); let isInvalid = range.end.compare( range.start ) > 7; return ( <MyRangeCalendar aria-label="Trip dates" value={range} onChange={setRange} isInvalid={isInvalid} errorMessage={isInvalid ? 'Maximum stay duration is 1 week' : undefined} /> ); } ## Trip dates, June 2025 ◀ ## June 2025 ▶ | S | M | T | W | T | F | S | | --- | --- | --- | --- | --- | --- | --- | | 1 | 2 | 3 | 4 | 5 | 6 | 7 | | 8 | 9 | 10 | 11 | 12 | 13 | 14 | | 15 | 16 | 17 | 18 | 19 | 20 | 21 | | 22 | 23 | 24 | 25 | 26 | 27 | 28 | | 29 | 30 | 1 | 2 | 3 | 4 | 5 | Maximum stay duration is 1 week Show CSS .react-aria-RangeCalendar { [slot=errorMessage] { font-size: 12px; color: var(--invalid-color); } } .react-aria-RangeCalendar { [slot=errorMessage] { font-size: 12px; color: var(--invalid-color); } } .react-aria-RangeCalendar { [slot=errorMessage] { font-size: 12px; color: var(--invalid-color); } } ## Controlling the focused date# * * * By default, the first selected date is focused when a `RangeCalendar` first mounts. If no `value` or `defaultValue` prop is provided, then the current date is focused. However, `RangeCalendar` supports controlling which date is focused using the `focusedValue` and `onFocusChange` props. This also determines which month is visible. The `defaultFocusedValue` prop allows setting the initial focused date when the `RangeCalendar` first mounts, without controlling it. This example focuses July 1, 2021 by default. The user may change the focused date, and the `onFocusChange` event updates the state. Clicking the button resets the focused date back to the initial value. import {CalendarDate} from '@internationalized/date'; function Example() { let defaultDate = new CalendarDate(2021, 7, 1); let [focusedDate, setFocusedDate] = React.useState(defaultDate); return ( <> <button style={{ marginBottom: 20 }} onClick={() => setFocusedDate(defaultDate)} > Reset focused date </button> <MyRangeCalendar focusedValue={focusedDate} onFocusChange={setFocusedDate} /> </> ); } import {CalendarDate} from '@internationalized/date'; function Example() { let defaultDate = new CalendarDate(2021, 7, 1); let [focusedDate, setFocusedDate] = React.useState( defaultDate ); return ( <> <button style={{ marginBottom: 20 }} onClick={() => setFocusedDate(defaultDate)} > Reset focused date </button> <MyRangeCalendar focusedValue={focusedDate} onFocusChange={setFocusedDate} /> </> ); } import {CalendarDate} from '@internationalized/date'; function Example() { let defaultDate = new CalendarDate( 2021, 7, 1 ); let [ focusedDate, setFocusedDate ] = React.useState( defaultDate ); return ( <> <button style={{ marginBottom: 20 }} onClick={() => setFocusedDate( defaultDate )} > Reset focused date </button> <MyRangeCalendar focusedValue={focusedDate} onFocusChange={setFocusedDate} /> </> ); } Reset focused date ## July 2021 ◀ ## July 2021 ▶ | S | M | T | W | T | F | S | | --- | --- | --- | --- | --- | --- | --- | | 27 | 28 | 29 | 30 | 1 | 2 | 3 | | 4 | 5 | 6 | 7 | 8 | 9 | 10 | | 11 | 12 | 13 | 14 | 15 | 16 | 17 | | 18 | 19 | 20 | 21 | 22 | 23 | 24 | | 25 | 26 | 27 | 28 | 29 | 30 | 31 | ## Disabled# * * * The `isDisabled` boolean prop makes the RangeCalendar disabled. Cells cannot be focused or selected. <MyRangeCalendar aria-label="Trip dates" isDisabled /> <MyRangeCalendar aria-label="Trip dates" isDisabled /> <MyRangeCalendar aria-label="Trip dates" isDisabled /> ## Trip dates, June 2025 ◀ ## June 2025 ▶ | S | M | T | W | T | F | S | | --- | --- | --- | --- | --- | --- | --- | | 1 | 2 | 3 | 4 | 5 | 6 | 7 | | 8 | 9 | 10 | 11 | 12 | 13 | 14 | | 15 | 16 | 17 | 18 | 19 | 20 | 21 | | 22 | 23 | 24 | 25 | 26 | 27 | 28 | | 29 | 30 | 1 | 2 | 3 | 4 | 5 | ### Read only# The `isReadOnly` boolean prop makes the RangeCalendar's value immutable. Unlike `isDisabled`, the RangeCalendar remains focusable. <MyRangeCalendar aria-label="Trip dates" value={{ start: today(getLocalTimeZone()), end: today(getLocalTimeZone()).add({ weeks: 1 }) }} isReadOnly /> <MyRangeCalendar aria-label="Trip dates" value={{ start: today(getLocalTimeZone()), end: today(getLocalTimeZone()).add({ weeks: 1 }) }} isReadOnly /> <MyRangeCalendar aria-label="Trip dates" value={{ start: today( getLocalTimeZone() ), end: today( getLocalTimeZone() ).add({ weeks: 1 }) }} isReadOnly /> ## Trip dates, June 2025 ◀ ## June 2025 ▶ | S | M | T | W | T | F | S | | --- | --- | --- | --- | --- | --- | --- | | 1 | 2 | 3 | 4 | 5 | 6 | 7 | | 8 | 9 | 10 | 11 | 12 | 13 | 14 | | 15 | 16 | 17 | 18 | 19 | 20 | 21 | | 22 | 23 | 24 | 25 | 26 | 27 | 28 | | 29 | 30 | 1 | 2 | 3 | 4 | 5 | ## Custom first day of week# * * * By default, the first day of the week is automatically set based on the current locale. This can be changed by setting the `firstDayOfWeek` prop to `'sun'`, `'mon'`, `'tue'`, `'wed'`, `'thu'`, `'fri'`, or `'sat'`. <MyRangeCalendar aria-label="Trip dates" firstDayOfWeek="mon" /> <MyRangeCalendar aria-label="Trip dates" firstDayOfWeek="mon" /> <MyRangeCalendar aria-label="Trip dates" firstDayOfWeek="mon" /> ## Trip dates, June 2025 ◀ ## June 2025 ▶ | M | T | W | T | F | S | S | | --- | --- | --- | --- | --- | --- | --- | | 26 | 27 | 28 | 29 | 30 | 31 | 1 | | 2 | 3 | 4 | 5 | 6 | 7 | 8 | | 9 | 10 | 11 | 12 | 13 | 14 | 15 | | 16 | 17 | 18 | 19 | 20 | 21 | 22 | | 23 | 24 | 25 | 26 | 27 | 28 | 29 | | 30 | 1 | 2 | 3 | 4 | 5 | 6 | ## Labeling# * * * An aria-label must be provided to the `RangeCalendar` for accessibility. If it is labeled by a separate element, an `aria-labelledby` prop must be provided using the `id` of the labeling element instead. ### Internationalization# In order to internationalize a `RangeCalendar`, a localized string should be passed to the `aria-label` prop. For languages that are read right-to-left (e.g. Hebrew and Arabic), keyboard navigation is automatically flipped. Ensure that your CSS accounts for this as well. Dates are automatically formatted using the current locale. ## Props# * * * ### RangeCalendar# | Name | Type | Default | Description | | --- | --- | --- | --- | | `visibleDuration` | ` DateDuration ` | `{months: 1}` | The amount of days that will be displayed at once. This affects how pagination works. | | `createCalendar` | `( (identifier: CalendarIdentifier )) => Calendar ` | — | A function to create a new Calendar object for a given calendar identifier. If not provided, the `createCalendar` function from `@internationalized/date` will be used. | | `allowsNonContiguousRanges` | `boolean` | — | When combined with `isDateUnavailable`, determines whether non-contiguous ranges, i.e. ranges containing unavailable dates, may be selected. | | `minValue` | ` DateValue | null` | — | The minimum allowed date that a user may select. | | `maxValue` | ` DateValue | null` | — | The maximum allowed date that a user may select. | | `isDateUnavailable` | `( (date: DateValue )) => boolean` | — | Callback that is called for each date of the calendar. If it returns true, then the date is unavailable. | | `isDisabled` | `boolean` | `false` | Whether the calendar is disabled. | | `isReadOnly` | `boolean` | `false` | Whether the calendar value is immutable. | | `autoFocus` | `boolean` | `false` | Whether to automatically focus the calendar when it mounts. | | `focusedValue` | ` DateValue | null` | — | Controls the currently focused date within the calendar. | | `defaultFocusedValue` | ` DateValue | null` | — | The date that is focused when the calendar first mounts (uncountrolled). | | `isInvalid` | `boolean` | — | Whether the current selection is invalid according to application logic. | | `pageBehavior` | ` PageBehavior ` | `visible` | Controls the behavior of paging. Pagination either works by advancing the visible page by visibleDuration (default) or one unit of visibleDuration. | | `firstDayOfWeek` | `'sun' | 'mon' | 'tue' | 'wed' | 'thu' | 'fri' | 'sat'` | — | The day that starts the week. | | `value` | ` RangeValue < DateValue > | null` | — | The current value (controlled). | | `defaultValue` | ` RangeValue < DateValue > | null` | — | The default value (uncontrolled). | | `children` | `ReactNode | ( (values: RangeCalendarRenderProps & & { defaultChildren: ReactNode | | undefined } )) => ReactNode` | — | The children of the component. A function may be provided to alter the children based on component state. | | `className` | `string | ( (values: RangeCalendarRenderProps & & { defaultClassName: string | | undefined } )) => string` | — | The CSS className for the element. A function may be provided to compute the class based on component state. | | `style` | `CSSProperties | ( (values: RangeCalendarRenderProps & & { defaultStyle: CSSProperties } )) => CSSProperties | undefined` | — | The inline style for the element. A function may be provided to compute the style based on component state. | Events | Name | Type | Description | | --- | --- | --- | | `onFocusChange` | `( (date: CalendarDate )) => void` | Handler that is called when the focused date changes. | | `onChange` | `( (value: RangeValue < MappedDateValue < DateValue >> )) => void` | Handler that is called when the value changes. | Layout | Name | Type | Description | | --- | --- | --- | | `slot` | `string | null` | A slot name for the component. Slots allow the component to receive props from a parent component. An explicit `null` value indicates that the local props completely override all props received from a parent. | Accessibility | Name | Type | Description | | --- | --- | --- | | `id` | `string` | The element's unique identifier. See MDN. | | `aria-label` | `string` | Defines a string value that labels the current element. | | `aria-labelledby` | `string` | Identifies the element (or elements) that labels the current element. | | `aria-describedby` | `string` | Identifies the element (or elements) that describes the object. | | `aria-details` | `string` | Identifies the element (or elements) that provide a detailed, extended description for the object. | ### Heading# A `<Heading>` accepts all HTML attributes. ### Button# A `<Button>` accepts its contents as `children`. Other props such as `onPress` and `isDisabled` will be set by the `RangeCalendar`. Show props | Name | Type | Default | Description | | --- | --- | --- | --- | | `form` | `string` | — | The `<form>` element to associate the button with. The value of this attribute must be the id of a `<form>` in the same document. | | `formAction` | `string` | — | The URL that processes the information submitted by the button. Overrides the action attribute of the button's form owner. | | `formEncType` | `string` | — | Indicates how to encode the form data that is submitted. | | `formMethod` | `string` | — | Indicates the HTTP method used to submit the form. | | `formNoValidate` | `boolean` | — | Indicates that the form is not to be validated when it is submitted. | | `formTarget` | `string` | — | Overrides the target attribute of the button's form owner. | | `name` | `string` | — | Submitted as a pair with the button's value as part of the form data. | | `value` | `string` | — | The value associated with the button's name when it's submitted with the form data. | | `isPending` | `boolean` | — | Whether the button is in a pending state. This disables press and hover events while retaining focusability, and announces the pending state to screen readers. | | `isDisabled` | `boolean` | — | Whether the button is disabled. | | `autoFocus` | `boolean` | — | Whether the element should receive focus on render. | | `type` | `'button' | 'submit' | 'reset'` | `'button'` | The behavior of the button when used in an HTML form. | | `children` | `ReactNode | ( (values: ButtonRenderProps & & { defaultChildren: ReactNode | | undefined } )) => ReactNode` | — | The children of the component. A function may be provided to alter the children based on component state. | | `className` | `string | ( (values: ButtonRenderProps & & { defaultClassName: string | | undefined } )) => string` | — | The CSS className for the element. A function may be provided to compute the class based on component state. | | `style` | `CSSProperties | ( (values: ButtonRenderProps & & { defaultStyle: CSSProperties } )) => CSSProperties | undefined` | — | The inline style for the element. A function may be provided to compute the style based on component state. | Events | Name | Type | Description | | --- | --- | --- | | `onPress` | `( (e: PressEvent )) => void` | Handler that is called when the press is released over the target. | | `onPressStart` | `( (e: PressEvent )) => void` | Handler that is called when a press interaction starts. | | `onPressEnd` | `( (e: PressEvent )) => void` | Handler that is called when a press interaction ends, either over the target or when the pointer leaves the target. | | `onPressChange` | `( (isPressed: boolean )) => void` | Handler that is called when the press state changes. | | `onPressUp` | `( (e: PressEvent )) => void` | Handler that is called when a press is released over the target, regardless of whether it started on the target or not. | | `onClick` | `( (e: MouseEvent<FocusableElement> )) => void` | **Not recommended – use `onPress` instead.** `onClick` is an alias for `onPress` provided for compatibility with other libraries. `onPress` provides additional event details for non-mouse interactions. | | `onFocus` | `( (e: FocusEvent<Target> )) => void` | Handler that is called when the element receives focus. | | `onBlur` | `( (e: FocusEvent<Target> )) => void` | Handler that is called when the element loses focus. | | `onFocusChange` | `( (isFocused: boolean )) => void` | Handler that is called when the element's focus status changes. | | `onKeyDown` | `( (e: KeyboardEvent )) => void` | Handler that is called when a key is pressed. | | `onKeyUp` | `( (e: KeyboardEvent )) => void` | Handler that is called when a key is released. | | `onHoverStart` | `( (e: HoverEvent )) => void` | Handler that is called when a hover interaction starts. | | `onHoverEnd` | `( (e: HoverEvent )) => void` | Handler that is called when a hover interaction ends. | | `onHoverChange` | `( (isHovering: boolean )) => void` | Handler that is called when the hover state changes. | Layout | Name | Type | Description | | --- | --- | --- | | `slot` | `string | null` | A slot name for the component. Slots allow the component to receive props from a parent component. An explicit `null` value indicates that the local props completely override all props received from a parent. | Accessibility | Name | Type | Description | | --- | --- | --- | | `id` | `string` | The element's unique identifier. See MDN. | | `excludeFromTabOrder` | `boolean` | Whether to exclude the element from the sequential tab order. If true, the element will not be focusable via the keyboard by tabbing. This should be avoided except in rare scenarios where an alternative means of accessing the element or its functionality via the keyboard is available. | | `preventFocusOnPress` | `boolean` | Whether to prevent focus from moving to the button when pressing it. Caution, this can make the button inaccessible and should only be used when alternative keyboard interaction is provided, such as ComboBox's MenuTrigger or a NumberField's increment/decrement control. | | `aria-expanded` | `boolean | 'true' | 'false'` | Indicates whether the element, or another grouping element it controls, is currently expanded or collapsed. | | `aria-haspopup` | `boolean | 'menu' | 'listbox' | 'tree' | 'grid' | 'dialog' | 'true' | 'false'` | Indicates the availability and type of interactive popup element, such as menu or dialog, that can be triggered by an element. | | `aria-controls` | `string` | Identifies the element (or elements) whose contents or presence are controlled by the current element. | | `aria-pressed` | `boolean | 'true' | 'false' | 'mixed'` | Indicates the current "pressed" state of toggle buttons. | | `aria-current` | `boolean | 'true' | 'false' | 'page' | 'step' | 'location' | 'date' | 'time'` | Indicates whether this element represents the current item within a container or set of related elements. | | `aria-label` | `string` | Defines a string value that labels the current element. | | `aria-labelledby` | `string` | Identifies the element (or elements) that labels the current element. | | `aria-describedby` | `string` | Identifies the element (or elements) that describes the object. | | `aria-details` | `string` | Identifies the element (or elements) that provide a detailed, extended description for the object. | ### CalendarGrid# A `<CalendarGrid>` renders an individual month within a `<RangeCalendar>`. It accepts a function as its `children`, which is called to render a `<CalendarCell>` for each date. This renders a default `<CalendarGridHeader>`, which displays the weekday names in the column headers. This can be customized by providing a `<CalendarGridHeader>` and `<CalendarGridBody>` as children instead of a function. | Name | Type | Default | Description | | --- | --- | --- | --- | | `children` | `ReactElement | ReactElement[] | ( (date: CalendarDate )) => ReactElement` | — | Either a function to render calendar cells for each date in the month, or children containing a `<CalendarGridHeader>`` and`<CalendarGridBody>\` when additional customization is needed. | | `offset` | ` DateDuration ` | — | An offset from the beginning of the visible date range that this CalendarGrid should display. Useful when displaying more than one month at a time. | | `weekdayStyle` | `'narrow' | 'short' | 'long'` | `"narrow"` | The style of weekday names to display in the calendar grid header, e.g. single letter, abbreviation, or full day name. | | `className` | `string` | — | The CSS className for the element. | | `style` | `CSSProperties` | — | The inline style for the element. | ### CalendarGridHeader# A `<CalendarGridHeader>` renders the header within a `<CalendarGrid>`, displaying a list of weekday names. It accepts a function as its `children`, which is called with a day name abbreviation to render a `<CalendarHeaderCell>` for each column header. Show props | Name | Type | Description | | --- | --- | --- | | `children` | `( (day: string )) => ReactElement` | A function to render a `<CalendarHeaderCell>` for a weekday name. | | `className` | `string` | The CSS className for the element. | | `style` | `CSSProperties` | The inline style for the element. | ### CalendarHeaderCell# A `<CalendarHeaderCell>` renders a column header within a `<CalendarGridHeader>`. It typically displays a weekday name. Show props | Name | Type | Description | | --- | --- | --- | | `children` | `ReactNode` | The children of the component. | | `className` | `string` | The CSS className for the element. | | `style` | `CSSProperties` | The inline style for the element. | Accessibility | Name | Type | Description | | --- | --- | --- | | `id` | `string` | The element's unique identifier. See MDN. | ### CalendarGridBody# A `<CalendarGridBody>` renders the body within a `<CalendarGrid>`. It accepts a function as its `children`, which is called to render a `<CalendarCell>` for each date. Show props | Name | Type | Description | | --- | --- | --- | | `children` | `( (date: CalendarDate )) => ReactElement` | A function to render a `<CalendarCell>` for a given date. | | `className` | `string` | The CSS className for the element. | | `style` | `CSSProperties` | The inline style for the element. | ### CalendarCell# A `<CalendarCell>` renders an individual date within a `<CalendarGrid>`. Show props | Name | Type | Description | | --- | --- | --- | | `date` | ` CalendarDate ` | The date to render in the cell. | | `children` | `ReactNode | ( (values: CalendarCellRenderProps & & { defaultChildren: ReactNode | | undefined } )) => ReactNode` | The children of the component. A function may be provided to alter the children based on component state. | | `className` | `string | ( (values: CalendarCellRenderProps & & { defaultClassName: string | | undefined } )) => string` | The CSS className for the element. A function may be provided to compute the class based on component state. | | `style` | `CSSProperties | ( (values: CalendarCellRenderProps & & { defaultStyle: CSSProperties } )) => CSSProperties | undefined` | The inline style for the element. A function may be provided to compute the style based on component state. | Events | Name | Type | Description | | --- | --- | --- | | `onHoverStart` | `( (e: HoverEvent )) => void` | Handler that is called when a hover interaction starts. | | `onHoverEnd` | `( (e: HoverEvent )) => void` | Handler that is called when a hover interaction ends. | | `onHoverChange` | `( (isHovering: boolean )) => void` | Handler that is called when the hover state changes. | ## Styling# * * * React Aria components can be styled in many ways, including using CSS classes, inline styles, utility classes (e.g. Tailwind), CSS-in-JS (e.g. Styled Components), etc. By default, all components include a builtin `className` attribute which can be targeted using CSS selectors. These follow the `react-aria-ComponentName` naming convention. .react-aria-RangeCalendar { /* ... */ } .react-aria-RangeCalendar { /* ... */ } .react-aria-RangeCalendar { /* ... */ } A custom `className` can also be specified on any component. This overrides the default `className` provided by React Aria with your own. <CalendarGrid className="my-calendar-grid"> {/* ... */} </CalendarGrid> <CalendarGrid className="my-calendar-grid"> {/* ... */} </CalendarGrid> <CalendarGrid className="my-calendar-grid"> {/* ... */} </CalendarGrid> In addition, some components support multiple UI states (e.g. focused, placeholder, readonly, etc.). React Aria components expose states using data attributes, which you can target in CSS selectors. For example: .react-aria-CalendarCell[data-selected] { /* ... */ } .react-aria-CalendarCell[data-invalid] { /* ... */ } .react-aria-CalendarCell[data-selected] { /* ... */ } .react-aria-CalendarCell[data-invalid] { /* ... */ } .react-aria-CalendarCell[data-selected] { /* ... */ } .react-aria-CalendarCell[data-invalid] { /* ... */ } The `className` and `style` props also accept functions which receive states for styling. This lets you dynamically determine the classes or styles to apply, which is useful when using utility CSS libraries like Tailwind. <CalendarCell className={({ isSelected }) => isSelected ? 'bg-blue-600' : 'bg-gray-600'} /> <CalendarCell className={({ isSelected }) => isSelected ? 'bg-blue-600' : 'bg-gray-600'} /> <CalendarCell className={( { isSelected } ) => isSelected ? 'bg-blue-600' : 'bg-gray-600'} /> Render props may also be used as children to alter what elements are rendered based on the current state. For example, you could add an additional element when a date is unavailable. <CalendarCell> {({formattedDate, isUnavailable}) => ( <> {isUnavailable && <UnavailableIcon />} <span>{formattedDate}</span> </> )} </CalendarCell> <CalendarCell> {({formattedDate, isUnavailable}) => ( <> {isUnavailable && <UnavailableIcon />} <span>{formattedDate}</span> </> )} </CalendarCell> <CalendarCell> {( { formattedDate, isUnavailable } ) => ( <> {isUnavailable && ( <UnavailableIcon /> )} <span> {formattedDate} </span> </> )} </CalendarCell> The states, selectors, and render props for each component used in a `RangeCalendar` are documented below. ### RangeCalendar# A `RangeCalendar` can be targeted with the `.react-aria-RangeCalendar` CSS selector, or by overriding with a custom `className`. It supports the following states and render props: | Name | CSS Selector | Description | | --- | --- | --- | | `state` | `—` | State of the range calendar. | | `isDisabled` | `[data-disabled]` | Whether the calendar is disabled. | | `isInvalid` | `[data-invalid]` | Whether the calendar is invalid. | ### Button# A Button can be targeted with the `.react-aria-Button` CSS selector, or by overriding with a custom `className`. The next and previous buttons can be targeted specifically with the `[slot=previous]` and `[slot=next]` selectors. Buttons support the following states: | Name | CSS Selector | Description | | --- | --- | --- | | `isHovered` | `[data-hovered]` | Whether the button is currently hovered with a mouse. | | `isPressed` | `[data-pressed]` | Whether the button is currently in a pressed state. | | `isFocused` | `[data-focused]` | Whether the button is focused, either via a mouse or keyboard. | | `isFocusVisible` | `[data-focus-visible]` | Whether the button is keyboard focused. | | `isDisabled` | `[data-disabled]` | Whether the button is disabled. | | `isPending` | `[data-pending]` | Whether the button is currently in a pending state. | ### CalendarGrid# A `CalendarGrid` can be targeted with the `.react-aria-CalendarGrid` CSS selector, or by overriding with a custom `className`. When a function is provided as children, a default `<CalendarGridHeader>` and `<CalendarGridBody>` are rendered. If you need to customize the styling of the header cells, you can render them yourself. import {CalendarGridBody, CalendarGridHeader, CalendarHeaderCell} from 'react-aria-components'; <RangeCalendar aria-label="Trip dates"> <header> <Button slot="previous">◀</Button> <Heading /> <Button slot="next">▶</Button> </header> <CalendarGrid> <CalendarGridHeader> {(day) => ( <CalendarHeaderCell style={{ color: 'var(--blue)' }}> {day} </CalendarHeaderCell> )} </CalendarGridHeader> <CalendarGridBody> {(date) => <CalendarCell date={date} />} </CalendarGridBody> </CalendarGrid> </RangeCalendar> import { CalendarGridBody, CalendarGridHeader, CalendarHeaderCell } from 'react-aria-components'; <RangeCalendar aria-label="Trip dates"> <header> <Button slot="previous">◀</Button> <Heading /> <Button slot="next">▶</Button> </header> <CalendarGrid> <CalendarGridHeader> {(day) => ( <CalendarHeaderCell style={{ color: 'var(--blue)' }} > {day} </CalendarHeaderCell> )} </CalendarGridHeader> <CalendarGridBody> {(date) => <CalendarCell date={date} />} </CalendarGridBody> </CalendarGrid> </RangeCalendar> import { CalendarGridBody, CalendarGridHeader, CalendarHeaderCell } from 'react-aria-components'; <RangeCalendar aria-label="Trip dates"> <header> <Button slot="previous"> ◀ </Button> <Heading /> <Button slot="next"> ▶ </Button> </header> <CalendarGrid> <CalendarGridHeader> {(day) => ( <CalendarHeaderCell style={{ color: 'var(--blue)' }} > {day} </CalendarHeaderCell> )} </CalendarGridHeader> <CalendarGridBody> {(date) => ( <CalendarCell date={date} /> )} </CalendarGridBody> </CalendarGrid> </RangeCalendar> ## Trip dates, June 2025 ◀ ## June 2025 ▶ | S | M | T | W | T | F | S | | --- | --- | --- | --- | --- | --- | --- | | 1 | 2 | 3 | 4 | 5 | 6 | 7 | | 8 | 9 | 10 | 11 | 12 | 13 | 14 | | 15 | 16 | 17 | 18 | 19 | 20 | 21 | | 22 | 23 | 24 | 25 | 26 | 27 | 28 | | 29 | 30 | 1 | 2 | 3 | 4 | 5 | ### CalendarGridHeader# A `CalendarGridHeader` can be targeted with the `.react-aria-CalendarGridHeader` CSS selector, or by overriding with a custom `className`. ### CalendarHeaderCell# A `CalendarHeaderCell` can be targeted with the `.react-aria-CalendarHeaderCell` CSS selector, or by overriding with a custom `className`. ### CalendarGridBody# A `CalendarGridBody` can be targeted with the `.react-aria-CalendarGridBody` CSS selector, or by overriding with a custom `className`. ### CalendarCell# A `CalendarCell` can be targeted with the `.react-aria-CalendarCell` CSS selector, or by overriding with a custom `className`. It supports the following states: | Name | CSS Selector | Description | | --- | --- | --- | | `date` | `—` | The date that the cell represents. | | `formattedDate` | `—` | The day number formatted according to the current locale. | | `isHovered` | `[data-hovered]` | Whether the cell is currently hovered with a mouse. | | `isPressed` | `[data-pressed]` | Whether the cell is currently being pressed. | | `isSelected` | `[data-selected]` | Whether the cell is selected. | | `isSelectionStart` | `[data-selection-start]` | Whether the cell is the first date in a range selection. | | `isSelectionEnd` | `[data-selection-end]` | Whether the cell is the last date in a range selection. | | `isFocused` | `[data-focused]` | Whether the cell is focused. | | `isFocusVisible` | `[data-focus-visible]` | Whether the cell is keyboard focused. | | `isDisabled` | `[data-disabled]` | Whether the cell is disabled, according to the calendar's `minValue`, `maxValue`, and `isDisabled` props. Disabled dates are not focusable, and cannot be selected by the user. They are typically displayed with a dimmed appearance. | | `isOutsideVisibleRange` | `[data-outside-visible-range]` | Whether the cell is outside the visible range of the calendar. For example, dates before the first day of a month in the same week. | | `isOutsideMonth` | `[data-outside-month]` | Whether the cell is outside the current month. | | `isUnavailable` | `[data-unavailable]` | Whether the cell is unavailable, according to the calendar's `isDateUnavailable` prop. Unavailable dates remain focusable, but cannot be selected by the user. They should be displayed with a visual affordance to indicate they are unavailable, such as a different color or a strikethrough. Note that because they are focusable, unavailable dates must meet a 4.5:1 color contrast ratio, as defined by WCAG. | | `isInvalid` | `[data-invalid]` | Whether the cell is part of an invalid selection. | ### Text# The error message element within a `RangeCalendar` can be targeted with the `[slot=errorMessage]` CSS selector, or by adding a custom `className`. ## Advanced customization# * * * ### Composition# If you need to customize one of the components within a `RangeCalendar`, such as `CalendarGrid` or `CalendarCell`, in many cases you can create a wrapper component. This lets you customize the props passed to the component. function MyCalendarCell(props) { return <CalendarCell {...props} className="my-item" /> } function MyCalendarCell(props) { return <CalendarCell {...props} className="my-item" /> } function MyCalendarCell( props ) { return ( <CalendarCell {...props} className="my-item" /> ); } ### Contexts# All React Aria Components export a corresponding context that can be used to send props to them from a parent element. This enables you to build your own compositional APIs similar to those found in React Aria Components itself. You can send any prop or ref via context that you could pass to the corresponding component. The local props and ref on the component are merged with the ones passed via context, with the local props taking precedence (following the rules documented in mergeProps). | Component | Context | Props | Ref | | --- | --- | --- | --- | | `RangeCalendar` | `RangeCalendarContext` | ` RangeCalendarProps ` | `HTMLDivElement` | This example uses `RangeCalendarContext` to create a composite component containing a calendar and buttons representing preset dates. The `useSlottedContext` hook can be used to consume contexts that support the `slot` prop. import {RangeCalendarContext, useSlottedContext} from 'react-aria-components'; function CalendarPicker({ children }) { let [value, onChange] = React.useState(null); let [focusedValue, onFocusChange] = React.useState(null); return ( <RangeCalendarContext.Provider value={{ value, onChange, focusedValue, onFocusChange }} > <div className="calendar-picker"> {children} </div> </RangeCalendarContext.Provider> ); } interface PresetProps { value: { start: CalendarDate; end: CalendarDate }; children: React.ReactNode; } function Preset({ value, children }: PresetProps) { let context = useSlottedContext(RangeCalendarContext)!; let onPress = () => { context.onFocusChange(value.start); context.onChange(value); }; return <Button onPress={onPress}>{children}</Button>; } import { RangeCalendarContext, useSlottedContext } from 'react-aria-components'; function CalendarPicker({ children }) { let [value, onChange] = React.useState(null); let [focusedValue, onFocusChange] = React.useState(null); return ( <RangeCalendarContext.Provider value={{ value, onChange, focusedValue, onFocusChange }} > <div className="calendar-picker"> {children} </div> </RangeCalendarContext.Provider> ); } interface PresetProps { value: { start: CalendarDate; end: CalendarDate }; children: React.ReactNode; } function Preset({ value, children }: PresetProps) { let context = useSlottedContext(RangeCalendarContext)!; let onPress = () => { context.onFocusChange(value.start); context.onChange(value); }; return <Button onPress={onPress}>{children}</Button>; } import { RangeCalendarContext, useSlottedContext } from 'react-aria-components'; function CalendarPicker( { children } ) { let [value, onChange] = React.useState(null); let [ focusedValue, onFocusChange ] = React.useState( null ); return ( <RangeCalendarContext.Provider value={{ value, onChange, focusedValue, onFocusChange }} > <div className="calendar-picker"> {children} </div> </RangeCalendarContext.Provider> ); } interface PresetProps { value: { start: CalendarDate; end: CalendarDate; }; children: React.ReactNode; } function Preset( { value, children }: PresetProps ) { let context = useSlottedContext( RangeCalendarContext )!; let onPress = () => { context .onFocusChange( value.start ); context.onChange( value ); }; return ( <Button onPress={onPress} > {children} </Button> ); } Now you can combine a `RangeCalendar` and one or more `Preset` components in a `CalendarPicker`. import {endOfMonth, endOfWeek, startOfMonth, startOfWeek} from '@internationalized/date'; import {useLocale} from 'react-aria'; function Example() { let { locale } = useLocale(); let now = today(getLocalTimeZone()); let nextMonth = now.add({ months: 1 }); return ( <CalendarPicker> <Preset value={{ start: startOfMonth(now), end: endOfMonth(now) }}> This month </Preset> <Preset value={{ start: startOfWeek(now, locale), end: endOfWeek(now, locale) }} > This week </Preset> <Preset value={{ start: startOfMonth(nextMonth), end: endOfMonth(nextMonth) }} > Next month </Preset> <MyRangeCalendar aria-label="Date filter" /> </CalendarPicker> ); } import { endOfMonth, endOfWeek, startOfMonth, startOfWeek } from '@internationalized/date'; import {useLocale} from 'react-aria'; function Example() { let { locale } = useLocale(); let now = today(getLocalTimeZone()); let nextMonth = now.add({ months: 1 }); return ( <CalendarPicker> <Preset value={{ start: startOfMonth(now), end: endOfMonth(now) }} > This month </Preset> <Preset value={{ start: startOfWeek(now, locale), end: endOfWeek(now, locale) }} > This week </Preset> <Preset value={{ start: startOfMonth(nextMonth), end: endOfMonth(nextMonth) }} > Next month </Preset> <MyRangeCalendar aria-label="Date filter" /> </CalendarPicker> ); } import { endOfMonth, endOfWeek, startOfMonth, startOfWeek } from '@internationalized/date'; import {useLocale} from 'react-aria'; function Example() { let { locale } = useLocale(); let now = today( getLocalTimeZone() ); let nextMonth = now .add({ months: 1 }); return ( <CalendarPicker> <Preset value={{ start: startOfMonth( now ), end: endOfMonth( now ) }} > This month </Preset> <Preset value={{ start: startOfWeek( now, locale ), end: endOfWeek( now, locale ) }} > This week </Preset> <Preset value={{ start: startOfMonth( nextMonth ), end: endOfMonth( nextMonth ) }} > Next month </Preset> <MyRangeCalendar aria-label="Date filter" /> </CalendarPicker> ); } This monthThis weekNext month ## Date filter, June 2025 ◀ ## June 2025 ▶ | S | M | T | W | T | F | S | | --- | --- | --- | --- | --- | --- | --- | | 1 | 2 | 3 | 4 | 5 | 6 | 7 | | 8 | 9 | 10 | 11 | 12 | 13 | 14 | | 15 | 16 | 17 | 18 | 19 | 20 | 21 | | 22 | 23 | 24 | 25 | 26 | 27 | 28 | | 29 | 30 | 1 | 2 | 3 | 4 | 5 | Show CSS .calendar-picker { > .react-aria-Button { margin: 0 4px 8px 4px; } } .calendar-picker { > .react-aria-Button { margin: 0 4px 8px 4px; } } .calendar-picker { > .react-aria-Button { margin: 0 4px 8px 4px; } } ### Custom children# RangeCalendar passes props to its child components, such as the heading and buttons, via their associated contexts. These contexts are exported so you can also consume them in your own custom components. This enables you to reuse existing components from your app or component library together with React Aria Components. | Component | Context | Props | Ref | | --- | --- | --- | --- | | `Heading` | `HeadingContext` | ` HeadingProps ` | `HTMLHeadingElement` | | `Button` | `ButtonContext` | ` ButtonProps ` | `HTMLButtonElement` | | `Text` | `TextContext` | ` TextProps ` | `HTMLElement` | This example consumes from `HeadingContext` in an existing styled heading component to make it compatible with React Aria Components. The `useContextProps` hook merges the local props and ref with the ones provided via context by RangeCalendar. import type {HeadingProps} from 'react-aria-components'; import {HeadingContext, useContextProps} from 'react-aria-components'; const MyCustomHeading = React.forwardRef( (props: HeadingProps, ref: React.ForwardedRef<HTMLHeadingElement>) => { // Merge the local props and ref with the ones provided via context. [props, ref] = useContextProps(props, ref, HeadingContext); // ... your existing Heading component return <h2 {...props} ref={ref} />; } ); import type {HeadingProps} from 'react-aria-components'; import { HeadingContext, useContextProps } from 'react-aria-components'; const MyCustomHeading = React.forwardRef( ( props: HeadingProps, ref: React.ForwardedRef<HTMLHeadingElement> ) => { // Merge the local props and ref with the ones provided via context. [props, ref] = useContextProps( props, ref, HeadingContext ); // ... your existing Heading component return <h2 {...props} ref={ref} />; } ); import type {HeadingProps} from 'react-aria-components'; import { HeadingContext, useContextProps } from 'react-aria-components'; const MyCustomHeading = React.forwardRef( ( props: HeadingProps, ref: React.ForwardedRef< HTMLHeadingElement > ) => { // Merge the local props and ref with the ones provided via context. [props, ref] = useContextProps( props, ref, HeadingContext ); // ... your existing Heading component return ( <h2 {...props} ref={ref} /> ); } ); Now you can use `MyCustomHeading` within a `RangeCalendar`, in place of the builtin React Aria Components `Heading`. <RangeCalendar> <MyCustomHeading /> {/* ... */} </RangeCalendar> <RangeCalendar> <MyCustomHeading /> {/* ... */} </RangeCalendar> <RangeCalendar> <MyCustomHeading /> {/* ... */} </RangeCalendar> ### State# RangeCalendar provides a `RangeCalendarState` object to its children via `RangeCalendarStateContext`. This can be used to access and manipulate the calendar's state. This example shows a `RangeCalendarValue` component that can be placed within a `RangeCalendar` to display the currently selected date as a formatted string. import {RangeCalendarStateContext} from 'react-aria-components'; import {useDateFormatter} from 'react-aria'; function RangeCalendarValue() { let state = React.useContext(RangeCalendarStateContext)!; let start = state.value?.start.toDate(getLocalTimeZone()); let end = state.value?.end.toDate(getLocalTimeZone()); let formatted = start && end ? useDateFormatter().formatRange(start, end) : 'None'; return <small>Selected date range: {formatted}</small>; } <RangeCalendar> <header> <Button slot="previous">◀</Button> <Heading /> <Button slot="next">▶</Button> </header> <CalendarGrid> {(date) => <CalendarCell date={date} />} </CalendarGrid> <RangeCalendarValue /></RangeCalendar> import {RangeCalendarStateContext} from 'react-aria-components'; import {useDateFormatter} from 'react-aria'; function RangeCalendarValue() { let state = React.useContext(RangeCalendarStateContext)!; let start = state.value?.start.toDate(getLocalTimeZone()); let end = state.value?.end.toDate(getLocalTimeZone()); let formatted = start && end ? useDateFormatter().formatRange(start, end) : 'None'; return <small>Selected date range: {formatted}</small>; } <RangeCalendar> <header> <Button slot="previous">◀</Button> <Heading /> <Button slot="next">▶</Button> </header> <CalendarGrid> {(date) => <CalendarCell date={date} />} </CalendarGrid> <RangeCalendarValue /></RangeCalendar> import {RangeCalendarStateContext} from 'react-aria-components'; import {useDateFormatter} from 'react-aria'; function RangeCalendarValue() { let state = React .useContext( RangeCalendarStateContext )!; let start = state.value ?.start.toDate( getLocalTimeZone() ); let end = state.value ?.end.toDate( getLocalTimeZone() ); let formatted = start && end ? useDateFormatter() .formatRange( start, end ) : 'None'; return ( <small> Selected date range: {formatted} </small> ); } <RangeCalendar> <header> <Button slot="previous"> ◀ </Button> <Heading /> <Button slot="next"> ▶ </Button> </header> <CalendarGrid> {(date) => ( <CalendarCell date={date} /> )} </CalendarGrid> <RangeCalendarValue /></RangeCalendar> ## June 2025 ◀ ## June 2025 ▶ | S | M | T | W | T | F | S | | --- | --- | --- | --- | --- | --- | --- | | 1 | 2 | 3 | 4 | 5 | 6 | 7 | | 8 | 9 | 10 | 11 | 12 | 13 | 14 | | 15 | 16 | 17 | 18 | 19 | 20 | 21 | | 22 | 23 | 24 | 25 | 26 | 27 | 28 | | 29 | 30 | 1 | 2 | 3 | 4 | 5 | Selected date range: None ### Hooks# If you need to customize things even further, such as accessing internal state or customizing DOM structure, you can drop down to the lower level Hook-based API. See useRangeCalendar for more details. --- ## Page: https://react-spectrum.adobe.com/react-aria/TimeField.html # TimeField A time field allows users to enter and edit time values using a keyboard. Each part of a time value is displayed in an individually editable segment. <table><tbody><tr><th>install</th><td><code>yarn add react-aria-components</code></td></tr><tr><th>version</th><td>1.10.1</td></tr><tr><th>usage</th><td><code><span>import</span> {TimeField} <span>from</span> <span>'react-aria-components'</span></code></td></tr></tbody></table> View repository GitHub View package NPM ## Example# * * * import {TimeField, Label, DateInput, DateSegment} from 'react-aria-components'; <TimeField> <Label>Event time</Label> <DateInput> {segment => <DateSegment segment={segment} />} </DateInput> </TimeField> import { DateInput, DateSegment, Label, TimeField } from 'react-aria-components'; <TimeField> <Label>Event time</Label> <DateInput> {(segment) => <DateSegment segment={segment} />} </DateInput> </TimeField> import { DateInput, DateSegment, Label, TimeField } from 'react-aria-components'; <TimeField> <Label> Event time </Label> <DateInput> {(segment) => ( <DateSegment segment={segment} /> )} </DateInput> </TimeField> Event time ––:–– AM Show CSS @import "@react-aria/example-theme"; .react-aria-TimeField { color: var(--text-color); display: flex; flex-direction: column; } .react-aria-DateInput { display: inline; padding: 4px; border: 1px solid var(--border-color); border-radius: 6px; background: var(--field-background); width: fit-content; min-width: 150px; white-space: nowrap; forced-color-adjust: none; &[data-focus-within] { outline: 2px solid var(--focus-ring-color); outline-offset: -1px; } } .react-aria-DateSegment { padding: 0 2px; font-variant-numeric: tabular-nums; text-align: end; color: var(--text-color); &[data-type=literal] { padding: 0; } &[data-placeholder] { color: var(--text-color-placeholder); font-style: italic; } &:focus { color: var(--highlight-foreground); background: var(--highlight-background); outline: none; border-radius: 4px; caret-color: transparent; } } @import "@react-aria/example-theme"; .react-aria-TimeField { color: var(--text-color); display: flex; flex-direction: column; } .react-aria-DateInput { display: inline; padding: 4px; border: 1px solid var(--border-color); border-radius: 6px; background: var(--field-background); width: fit-content; min-width: 150px; white-space: nowrap; forced-color-adjust: none; &[data-focus-within] { outline: 2px solid var(--focus-ring-color); outline-offset: -1px; } } .react-aria-DateSegment { padding: 0 2px; font-variant-numeric: tabular-nums; text-align: end; color: var(--text-color); &[data-type=literal] { padding: 0; } &[data-placeholder] { color: var(--text-color-placeholder); font-style: italic; } &:focus { color: var(--highlight-foreground); background: var(--highlight-background); outline: none; border-radius: 4px; caret-color: transparent; } } @import "@react-aria/example-theme"; .react-aria-TimeField { color: var(--text-color); display: flex; flex-direction: column; } .react-aria-DateInput { display: inline; padding: 4px; border: 1px solid var(--border-color); border-radius: 6px; background: var(--field-background); width: fit-content; min-width: 150px; white-space: nowrap; forced-color-adjust: none; &[data-focus-within] { outline: 2px solid var(--focus-ring-color); outline-offset: -1px; } } .react-aria-DateSegment { padding: 0 2px; font-variant-numeric: tabular-nums; text-align: end; color: var(--text-color); &[data-type=literal] { padding: 0; } &[data-placeholder] { color: var(--text-color-placeholder); font-style: italic; } &:focus { color: var(--highlight-foreground); background: var(--highlight-background); outline: none; border-radius: 4px; caret-color: transparent; } } ## Features# * * * A time field can be built using `<input type="time">`, but this is very limited in functionality, lacking in internationalization capabilities, inconsistent between browsers, and difficult to style. `TimeField` helps achieve accessible and international time fields that can be styled as needed. * **International** – Support for locale-specific formatting, number systems, hour cycles, and right-to-left layout. * **Time zone aware** – Times can optionally include a time zone. All modifications follow time zone rules such as daylight saving time. * **Accessible** – Each time unit is displayed as an individually focusable and editable segment, which allows users an easy way to edit times using the keyboard, in any format and locale. * **Touch friendly** – Time segments are editable using an easy to use numeric keypad, and all interactions are accessible using touch-based screen readers. * **Validation** – Integrates with HTML forms, supporting required, minimum and maximum values, custom validation functions, realtime validation, and server-side validation errors. * **Customizable** – As with all of React Aria, the DOM structure and styling of all elements can be fully customized. Read our blog post for more details about the internationalization, accessibility, and user experience features implemented by `TimeField`. ## Anatomy# * * * A time field consists of a label, and a group of segments representing each unit of a time (e.g. hours, minutes, and seconds). Each segment is individually focusable and editable by the user, by typing or using the arrow keys to increment and decrement the value. This approach allows values to be formatted and parsed correctly regardless of the locale or time format, and offers an easy and error-free way to edit times using the keyboard. `TimeField` also supports optional description and error message elements, which can be used to provide more context about the field, and any validation messages. These are linked with the input via the `aria-describedby` attribute. import {DateInput, DateSegment, FieldError, Label, Text, TimeField} from 'react-aria-components'; <TimeField> <Label /> <DateInput> {(segment) => <DateSegment segment={segment} />} </DateInput> <Text slot="description" /> <FieldError /> </TimeField> import { DateInput, DateSegment, FieldError, Label, Text, TimeField } from 'react-aria-components'; <TimeField> <Label /> <DateInput> {(segment) => <DateSegment segment={segment} />} </DateInput> <Text slot="description" /> <FieldError /> </TimeField> import { DateInput, DateSegment, FieldError, Label, Text, TimeField } from 'react-aria-components'; <TimeField> <Label /> <DateInput> {(segment) => ( <DateSegment segment={segment} /> )} </DateInput> <Text slot="description" /> <FieldError /> </TimeField> If the time field does not have a visible label, an `aria-label` or `aria-labelledby` prop must be passed instead to identify it to assistive technology. Note that most of this anatomy is shared with DateField, so you can reuse many components between them if you have both. ### Concepts# `TimeField` makes use of the following concepts: @internationalized/date Represent and manipulate dates and times in a locale-aware manner. Forms Validating and submitting form data, and integrating with form libraries. ### Composed components# A `TimeField` uses the following components, which may also be used standalone or reused in other components. Label A label provides context for an input element. ## Starter kits# * * * To help kick-start your project, we offer starter kits that include example implementations of all React Aria components with various styling solutions. All components are fully styled, including support for dark mode, high contrast mode, and all UI states. Each starter comes with a pre-configured Storybook that you can experiment with, or use as a starting point for your own component library. Vanilla CSS Download ZIP Preview Tailwind CSS Download ZIP Preview ## Reusable wrappers# * * * If you will use a TimeField in multiple places in your app, you can wrap all of the pieces into a reusable component. This way, the DOM structure, styling code, and other logic are defined in a single place and reused everywhere to ensure consistency. This example wraps `TimeField` and all of its children together into a single component which accepts a `label` prop, which is passed to the right place. It also shows how to use the `description` slot to render help text, and `FieldError` component to render validation errors. import type {TimeFieldProps, TimeValue, ValidationResult} from 'react-aria-components'; import {FieldError, Text} from 'react-aria-components'; interface MyTimeFieldProps<T extends TimeValue> extends TimeFieldProps<T> { label?: string; description?: string; errorMessage?: string | ((validation: ValidationResult) => string); } function MyTimeField<T extends TimeValue>( { label, description, errorMessage, ...props }: MyTimeFieldProps<T> ) { return ( <TimeField {...props}> <Label>{label}</Label> <DateInput> {(segment) => <DateSegment segment={segment} />} </DateInput> {description && <Text slot="description">{description}</Text>} <FieldError>{errorMessage}</FieldError> </TimeField> ); } <MyTimeField label="Event time" /> import type { TimeFieldProps, TimeValue, ValidationResult } from 'react-aria-components'; import {FieldError, Text} from 'react-aria-components'; interface MyTimeFieldProps<T extends TimeValue> extends TimeFieldProps<T> { label?: string; description?: string; errorMessage?: | string | ((validation: ValidationResult) => string); } function MyTimeField<T extends TimeValue>( { label, description, errorMessage, ...props }: MyTimeFieldProps<T> ) { return ( <TimeField {...props}> <Label>{label}</Label> <DateInput> {(segment) => <DateSegment segment={segment} />} </DateInput> {description && ( <Text slot="description">{description}</Text> )} <FieldError>{errorMessage}</FieldError> </TimeField> ); } <MyTimeField label="Event time" /> import type { TimeFieldProps, TimeValue, ValidationResult } from 'react-aria-components'; import { FieldError, Text } from 'react-aria-components'; interface MyTimeFieldProps< T extends TimeValue > extends TimeFieldProps<T> { label?: string; description?: string; errorMessage?: | string | (( validation: ValidationResult ) => string); } function MyTimeField< T extends TimeValue >({ label, description, errorMessage, ...props }: MyTimeFieldProps<T>) { return ( <TimeField {...props} > <Label> {label} </Label> <DateInput> {(segment) => ( <DateSegment segment={segment} /> )} </DateInput> {description && ( <Text slot="description"> {description} </Text> )} <FieldError> {errorMessage} </FieldError> </TimeField> ); } <MyTimeField label="Event time" /> Event time ––:–– AM ## Value# * * * A `TimeField` displays a placeholder by default. An initial, uncontrolled value can be provided to the `TimeField` using the `defaultValue` prop. Alternatively, a controlled value can be provided using the `value` prop. Time values are provided using objects in the @internationalized/date package. This library handles correct international date and time manipulation across calendars, time zones, and other localization concerns. `TimeField` only supports selecting times, but values with date components are also accepted. By default, `TimeField` will emit `Time` objects in the `onChange` event, but if a` CalendarDateTime `or` ZonedDateTime `object is passed as the `value` or `defaultValue`, values of that type will be emitted, changing only the time and preserving the date components. import {Time} from '@internationalized/date'; function Example() { let [value, setValue] = React.useState(new Time(11, 45)); return ( <> <MyTimeField label="Time (uncontrolled)" defaultValue={new Time(11, 45)} /> <MyTimeField label="Time (controlled)" value={value} onChange={setValue} /> </> ); } import {Time} from '@internationalized/date'; function Example() { let [value, setValue] = React.useState(new Time(11, 45)); return ( <> <MyTimeField label="Time (uncontrolled)" defaultValue={new Time(11, 45)} /> <MyTimeField label="Time (controlled)" value={value} onChange={setValue} /> </> ); } import {Time} from '@internationalized/date'; function Example() { let [value, setValue] = React.useState( new Time(11, 45) ); return ( <> <MyTimeField label="Time (uncontrolled)" defaultValue={new Time( 11, 45 )} /> <MyTimeField label="Time (controlled)" value={value} onChange={setValue} /> </> ); } Time (uncontrolled) 11:45 AM Time (controlled) 11:45 AM `Time` values may also be parsed from strings using the `parseTime` function. This accepts ISO 8601 formatted time strings such as `"04:45:23.123"`. The `toString` method of a `Time` object can also be used to convert a time object to a string. ### Time zones# `TimeField` is time zone aware when a `ZonedDateTime` object is provided as the value. In this case, the time zone abbreviation is displayed, and time zone concerns such as daylight saving time are taken into account when the value is manipulated. In most cases, your data will come from and be sent to a server as an ISO 8601 formatted string. @internationalized/date includes functions for parsing strings in multiple formats into `ZonedDateTime` objects. Which format you use will depend on what information you need to store. * ` parseZonedDateTime `– This function parses a date with an explicit time zone and optional UTC offset attached (e.g. `"2021-11-07T00:45[America/Los_Angeles]"` or `"2021-11-07T00:45-07:00[America/Los_Angeles]"`). This format preserves the maximum amount of information. If the exact local time and time zone that a user selected is important, use this format. Storing the time zone and offset that was selected rather than converting to UTC ensures that the local time is correct regardless of daylight saving rule changes (e.g. if a locale abolishes DST). Examples where this applies include calendar events, reminders, and other times that occur in a particular location. * ` parseAbsolute `– This function parses an absolute date and time that occurs at the same instant at all locations on Earth. It can be represented in UTC (e.g. `"2021-11-07T07:45:00Z"`), or stored with a particular offset (e.g. `"2021-11-07T07:45:00-07:00"`). A time zone identifier, e.g. `America/Los_Angeles`, must be passed, and the result will be converted into that time zone. Absolute times are the best way to represent events that occurred in the past, or future events where an exact time is needed, regardless of time zone. * ` parseAbsoluteToLocal `– This function parses an absolute date and time into the current user's local time zone. It is a shortcut for `parseAbsolute`, and accepts the same formats. import {parseZonedDateTime} from '@internationalized/date'; <MyTimeField label="Event time" defaultValue={parseZonedDateTime('2022-11-07T00:45[America/Los_Angeles]')} /> import {parseZonedDateTime} from '@internationalized/date'; <MyTimeField label="Event time" defaultValue={parseZonedDateTime( '2022-11-07T00:45[America/Los_Angeles]' )} /> import {parseZonedDateTime} from '@internationalized/date'; <MyTimeField label="Event time" defaultValue={parseZonedDateTime( '2022-11-07T00:45[America/Los_Angeles]' )} /> Event time 12:45 AM PST `TimeField` displays times in the time zone included in the `ZonedDateTime` object. The above example is always displayed in Pacific Standard Time because the `America/Los_Angeles` time zone identifier is provided. @internationalized/date includes functions for converting dates between time zones, or parsing a date directly into a specific time zone or the user's local time zone, as shown below. import {parseAbsoluteToLocal} from '@internationalized/date'; <MyTimeField label="Event time" defaultValue={parseAbsoluteToLocal('2021-11-07T07:45:00Z')} /> import {parseAbsoluteToLocal} from '@internationalized/date'; <MyTimeField label="Event time" defaultValue={parseAbsoluteToLocal( '2021-11-07T07:45:00Z' )} /> import {parseAbsoluteToLocal} from '@internationalized/date'; <MyTimeField label="Event time" defaultValue={parseAbsoluteToLocal( '2021-11-07T07:45:00Z' )} /> Event time 7:45 AM UTC ### Granularity# The `granularity` prop allows you to control the smallest unit that is displayed by `TimeField`. By default, times are displayed with `"minute"` granularity. More granular time values can be displayed by setting the `granularity` prop to `"second"`. <MyTimeField label="Event time" granularity="second" defaultValue={parseAbsoluteToLocal('2021-04-07T18:45:22Z')} /> <MyTimeField label="Event time" granularity="second" defaultValue={parseAbsoluteToLocal( '2021-04-07T18:45:22Z' )} /> <MyTimeField label="Event time" granularity="second" defaultValue={parseAbsoluteToLocal( '2021-04-07T18:45:22Z' )} /> Event time 6:45:22 PM UTC ### HTML forms# TimeField supports the `name` prop for integration with HTML forms. The value will be submitted to the server as an ISO 8601 formatted string, e.g. `"08:45:00"`. <MyTimeField label="Meeting time" name="meetingTime" /> <MyTimeField label="Meeting time" name="meetingTime" /> <MyTimeField label="Meeting time" name="meetingTime" /> Meeting time ––:–– AM ## Events# * * * `TimeField` accepts an `onChange` prop which is triggered whenever the time is edited by the user. The example below uses `onChange` to update a separate element with a formatted version of the date in the user's locale and local time zone. This is done by converting the date to a native JavaScript `Date` object to pass to the formatter. `TimeField` allows editing the time components while keeping the date fixed. import {useDateFormatter} from 'react-aria'; function Example() { let [date, setDate] = React.useState( parseAbsoluteToLocal('2021-04-07T18:45:22Z') ); let formatter = useDateFormatter({ dateStyle: 'long', timeStyle: 'long' }); return ( <> <MyTimeField label="Time" value={date} onChange={setDate} /> <p> Selected date and time:{' '} {(date?.toDate && formatter.format(date.toDate())) || (date && date.toString()) || '--'} </p> </> ); } import {useDateFormatter} from 'react-aria'; function Example() { let [date, setDate] = React.useState( parseAbsoluteToLocal('2021-04-07T18:45:22Z') ); let formatter = useDateFormatter({ dateStyle: 'long', timeStyle: 'long' }); return ( <> <MyTimeField label="Time" value={date} onChange={setDate} /> <p> Selected date and time:{' '} {(date?.toDate && formatter.format(date.toDate())) || (date && date.toString()) || '--'} </p> </> ); } import {useDateFormatter} from 'react-aria'; function Example() { let [date, setDate] = React.useState( parseAbsoluteToLocal( '2021-04-07T18:45:22Z' ) ); let formatter = useDateFormatter({ dateStyle: 'long', timeStyle: 'long' }); return ( <> <MyTimeField label="Time" value={date} onChange={setDate} /> <p> Selected date and time:{' '} {(date?.toDate && formatter .format( date .toDate() )) || (date && date .toString()) || '--'} </p> </> ); } Time 6:45 PM UTC Selected date and time: April 7, 2021 at 6:45:22 PM UTC ## Validation# * * * TimeField supports the `isRequired` prop to ensure the user enters a value, as well as minimum and maximum values, and custom client and server-side validation. It can also be integrated with other form libraries. See the Forms guide to learn more. To display validation errors, add a `<FieldError>` element as a child of the TimeField. This allows you to render error messages from all of the above sources with consistent custom styles. import {Form, FieldError, Button} from 'react-aria-components'; <Form> <TimeField name="time" isRequired> <Label>Meeting time</Label> <DateInput> {segment => <DateSegment segment={segment} />} </DateInput> <FieldError /> </TimeField> <Button type="submit">Submit</Button> </Form> import { Button, FieldError, Form } from 'react-aria-components'; <Form> <TimeField name="time" isRequired> <Label>Meeting time</Label> <DateInput> {(segment) => <DateSegment segment={segment} />} </DateInput> <FieldError /> </TimeField> <Button type="submit">Submit</Button> </Form> import { Button, FieldError, Form } from 'react-aria-components'; <Form> <TimeField name="time" isRequired > <Label> Meeting time </Label> <DateInput> {(segment) => ( <DateSegment segment={segment} /> )} </DateInput> <FieldError /> </TimeField> <Button type="submit"> Submit </Button> </Form> Meeting time ––:–– AM Submit Show CSS .react-aria-DateSegment { &[data-invalid] { color: var(--invalid-color); &:focus { background: var(--highlight-background-invalid); color: var(--highlight-foreground); } } } .react-aria-TimeField { .react-aria-FieldError { font-size: 12px; color: var(--invalid-color); } } .react-aria-DateSegment { &[data-invalid] { color: var(--invalid-color); &:focus { background: var(--highlight-background-invalid); color: var(--highlight-foreground); } } } .react-aria-TimeField { .react-aria-FieldError { font-size: 12px; color: var(--invalid-color); } } .react-aria-DateSegment { &[data-invalid] { color: var(--invalid-color); &:focus { background: var(--highlight-background-invalid); color: var(--highlight-foreground); } } } .react-aria-TimeField { .react-aria-FieldError { font-size: 12px; color: var(--invalid-color); } } By default, `FieldError` displays default validation messages provided by the browser. See Customizing error messages in the Forms guide to learn how to provide your own custom errors. ### Minimum and maximum values# The `minValue` and `maxValue` props can also be used to ensure the value is within a specific range. This example only accepts times between 9 AM and 5 PM. <Form> <MyTimeField label="Meeting time" minValue={new Time(9)} maxValue={new Time(17)} defaultValue={new Time(8)} /> <Button type="submit">Submit</Button> </Form> <Form> <MyTimeField label="Meeting time" minValue={new Time(9)} maxValue={new Time(17)} defaultValue={new Time(8)} /> <Button type="submit">Submit</Button> </Form> <Form> <MyTimeField label="Meeting time" minValue={new Time( 9 )} maxValue={new Time( 17 )} defaultValue={new Time( 8 )} /> <Button type="submit"> Submit </Button> </Form> Meeting time 8:00 AM Submit ### Custom validation# The `validate` function can be used to perform custom validation logic. It receives the current field value, and should return a string or array of strings representing one or more error messages if the value is invalid. This example validates that the selected time is on a 15 minute increment. <Form> <MyTimeField label="Meeting time" validate={(time) => time?.minute % 15 !== 0 ? 'Meetings start every 15 minutes.' : null} defaultValue={new Time(9, 25)} /> <Button type="submit">Submit</Button> </Form> <Form> <MyTimeField label="Meeting time" validate={(time) => time?.minute % 15 !== 0 ? 'Meetings start every 15 minutes.' : null} defaultValue={new Time(9, 25)} /> <Button type="submit">Submit</Button> </Form> <Form> <MyTimeField label="Meeting time" validate={(time) => time?.minute % 15 !== 0 ? 'Meetings start every 15 minutes.' : null} defaultValue={new Time( 9, 25 )} /> <Button type="submit"> Submit </Button> </Form> Meeting time 9:25 AM Submit ### Description# The `description` slot can be used to associate additional help text with a date field. <TimeField> <Label>Appointment time</Label> <DateInput> {segment => <DateSegment segment={segment} />} </DateInput> <Text slot="description">Please select a time between 9 AM and 5 PM.</Text></TimeField> <TimeField> <Label>Appointment time</Label> <DateInput> {(segment) => <DateSegment segment={segment} />} </DateInput> <Text slot="description"> Please select a time between 9 AM and 5 PM. </Text></TimeField> <TimeField> <Label> Appointment time </Label> <DateInput> {(segment) => ( <DateSegment segment={segment} /> )} </DateInput> <Text slot="description"> Please select a time between 9 AM and 5 PM. </Text></TimeField> Appointment time ––:–– AM Please select a time between 9 AM and 5 PM. Show CSS .react-aria-TimeField { [slot=description] { font-size: 12px; } } .react-aria-TimeField { [slot=description] { font-size: 12px; } } .react-aria-TimeField { [slot=description] { font-size: 12px; } } ## Format options# * * * ### Placeholder value# When no value is set, a placeholder is shown. The format of the placeholder is influenced by the `granularity` and `placeholderValue` props. `placeholderValue` also controls the default values of each segment when the user first interacts with them, e.g. using the up and down arrow keys. By default, the `placeholderValue` is midnight, but you can set it to a more appropriate value if needed. <MyTimeField label="Meeting time" placeholderValue={new Time(9)} /> <MyTimeField label="Meeting time" placeholderValue={new Time(9)} /> <MyTimeField label="Meeting time" placeholderValue={new Time( 9 )} /> Meeting time ––:–– AM ### Hide time zone# When a `ZonedDateTime` object is provided as the value to `TimeField`, the time zone abbreviation is displayed by default. However, if this is displayed elsewhere or implicit based on the usecase, it can be hidden using the `hideTimeZone` option. <MyTimeField label="Appointment time" defaultValue={parseZonedDateTime('2022-11-07T10:45[America/Los_Angeles]')} hideTimeZone /> <MyTimeField label="Appointment time" defaultValue={parseZonedDateTime( '2022-11-07T10:45[America/Los_Angeles]' )} hideTimeZone /> <MyTimeField label="Appointment time" defaultValue={parseZonedDateTime( '2022-11-07T10:45[America/Los_Angeles]' )} hideTimeZone /> Appointment time 10:45 AM ### Hour cycle# By default, `TimeField` displays times in either 12 or 24 hour hour format depending on the user's locale. However, this can be overridden using the `hourCycle` prop if needed for a specific usecase. This example forces `TimeField` to use 24-hour time, regardless of the locale. <MyTimeField label="Appointment time" hourCycle={24} /> <MyTimeField label="Appointment time" hourCycle={24} /> <MyTimeField label="Appointment time" hourCycle={24} /> Appointment time ––:–– ## Props# * * * ### TimeField# | Name | Type | Default | Description | | --- | --- | --- | --- | | `hourCycle` | `12 | 24` | — | Whether to display the time in 12 or 24 hour format. By default, this is determined by the user's locale. | | `granularity` | `'hour' | 'minute' | 'second'` | `'minute'` | Determines the smallest unit that is displayed in the time picker. | | `hideTimeZone` | `boolean` | — | Whether to hide the time zone abbreviation. | | `shouldForceLeadingZeros` | `boolean` | — | Whether to always show leading zeros in the hour field. By default, this is determined by the user's locale. | | `placeholderValue` | ` TimeValue ` | — | A placeholder time that influences the format of the placeholder shown when no value is selected. Defaults to 12:00 AM or 00:00 depending on the hour cycle. | | `minValue` | ` TimeValue | null` | — | The minimum allowed time that a user may select. | | `maxValue` | ` TimeValue | null` | — | The maximum allowed time that a user may select. | | `isDisabled` | `boolean` | — | Whether the input is disabled. | | `isReadOnly` | `boolean` | — | Whether the input can be selected but not changed by the user. | | `isRequired` | `boolean` | — | Whether user input is required on the input before form submission. | | `isInvalid` | `boolean` | — | Whether the input value is invalid. | | `validate` | `( (value: MappedTimeValue < TimeValue > )) => ValidationError | true | null | undefined` | — | A function that returns an error message if a given value is invalid. Validation errors are displayed to the user when the form is submitted if `validationBehavior="native"`. For realtime validation, use the `isInvalid` prop instead. | | `autoFocus` | `boolean` | — | Whether the element should receive focus on render. | | `value` | ` TimeValue | null` | — | The current value (controlled). | | `defaultValue` | ` TimeValue | null` | — | The default value (uncontrolled). | | `name` | `string` | — | The name of the input element, used when submitting an HTML form. See MDN. | | `validationBehavior` | `'native' | 'aria'` | `'native'` | Whether to use native HTML form validation to prevent form submission when the value is missing or invalid, or mark the field as required or invalid via ARIA. | | `children` | `ReactNode | ( (values: DateFieldRenderProps & & { defaultChildren: ReactNode | | undefined } )) => ReactNode` | — | The children of the component. A function may be provided to alter the children based on component state. | | `className` | `string | ( (values: DateFieldRenderProps & & { defaultClassName: string | | undefined } )) => string` | — | The CSS className for the element. A function may be provided to compute the class based on component state. | | `style` | `CSSProperties | ( (values: DateFieldRenderProps & & { defaultStyle: CSSProperties } )) => CSSProperties | undefined` | — | The inline style for the element. A function may be provided to compute the style based on component state. | Events | Name | Type | Description | | --- | --- | --- | | `onFocus` | `( (e: FocusEvent<Target> )) => void` | Handler that is called when the element receives focus. | | `onBlur` | `( (e: FocusEvent<Target> )) => void` | Handler that is called when the element loses focus. | | `onFocusChange` | `( (isFocused: boolean )) => void` | Handler that is called when the element's focus status changes. | | `onKeyDown` | `( (e: KeyboardEvent )) => void` | Handler that is called when a key is pressed. | | `onKeyUp` | `( (e: KeyboardEvent )) => void` | Handler that is called when a key is released. | | `onChange` | `( (value: MappedTimeValue < TimeValue > | | null )) => void` | Handler that is called when the value changes. | Layout | Name | Type | Description | | --- | --- | --- | | `slot` | `string | null` | A slot name for the component. Slots allow the component to receive props from a parent component. An explicit `null` value indicates that the local props completely override all props received from a parent. | Accessibility | Name | Type | Description | | --- | --- | --- | | `id` | `string` | The element's unique identifier. See MDN. | | `aria-label` | `string` | Defines a string value that labels the current element. | | `aria-labelledby` | `string` | Identifies the element (or elements) that labels the current element. | | `aria-describedby` | `string` | Identifies the element (or elements) that describes the object. | | `aria-details` | `string` | Identifies the element (or elements) that provide a detailed, extended description for the object. | ### Label# A `<Label>` accepts all HTML attributes. ### DateInput# The `<DateInput>` component renders a group of date segments. It accepts a function as its `children`, which is called to render a `<DateSegment>` for each segment. Show props | Name | Type | Description | | --- | --- | --- | | `children` | `( (segment: DateSegment )) => ReactElement` | | | `className` | `string | ( (values: DateInputRenderProps & & { defaultClassName: string | | undefined } )) => string` | The CSS className for the element. A function may be provided to compute the class based on component state. | | `style` | `CSSProperties | ( (values: DateInputRenderProps & & { defaultStyle: CSSProperties } )) => CSSProperties | undefined` | The inline style for the element. A function may be provided to compute the style based on component state. | Layout | Name | Type | Description | | --- | --- | --- | | `slot` | `string | null` | A slot name for the component. Slots allow the component to receive props from a parent component. An explicit `null` value indicates that the local props completely override all props received from a parent. | ### DateSegment# The `<DateSegment>` component renders an individual segment. Show props | Name | Type | Description | | --- | --- | --- | | `segment` | ` DateSegment ` | | | `children` | `ReactNode | ( (values: DateSegmentRenderProps & & { defaultChildren: ReactNode | | undefined } )) => ReactNode` | The children of the component. A function may be provided to alter the children based on component state. | | `className` | `string | ( (values: DateSegmentRenderProps & & { defaultClassName: string | | undefined } )) => string` | The CSS className for the element. A function may be provided to compute the class based on component state. | | `style` | `CSSProperties | ( (values: DateSegmentRenderProps & & { defaultStyle: CSSProperties } )) => CSSProperties | undefined` | The inline style for the element. A function may be provided to compute the style based on component state. | Events | Name | Type | Description | | --- | --- | --- | | `onHoverStart` | `( (e: HoverEvent )) => void` | Handler that is called when a hover interaction starts. | | `onHoverEnd` | `( (e: HoverEvent )) => void` | Handler that is called when a hover interaction ends. | | `onHoverChange` | `( (isHovering: boolean )) => void` | Handler that is called when the hover state changes. | ### FieldError# A `<FieldError>` displays validation errors. Show props | Name | Type | Description | | --- | --- | --- | | `children` | `ReactNode | ( (values: FieldErrorRenderProps & & { defaultChildren: ReactNode | | undefined } )) => ReactNode` | The children of the component. A function may be provided to alter the children based on component state. | | `className` | `string | ( (values: FieldErrorRenderProps & & { defaultClassName: string | | undefined } )) => string` | The CSS className for the element. A function may be provided to compute the class based on component state. | | `style` | `CSSProperties | ( (values: FieldErrorRenderProps & & { defaultStyle: CSSProperties } )) => CSSProperties | undefined` | The inline style for the element. A function may be provided to compute the style based on component state. | Accessibility | Name | Type | Description | | --- | --- | --- | | `id` | `string` | The element's unique identifier. See MDN. | ## Styling# * * * React Aria components can be styled in many ways, including using CSS classes, inline styles, utility classes (e.g. Tailwind), CSS-in-JS (e.g. Styled Components), etc. By default, all components include a builtin `className` attribute which can be targeted using CSS selectors. These follow the `react-aria-ComponentName` naming convention. .react-aria-TimeField { /* ... */ } .react-aria-TimeField { /* ... */ } .react-aria-TimeField { /* ... */ } A custom `className` can also be specified on any component. This overrides the default `className` provided by React Aria with your own. <DateInput className="my-date-input"> {/* ... */} </DateInput> <DateInput className="my-date-input"> {/* ... */} </DateInput> <DateInput className="my-date-input"> {/* ... */} </DateInput> In addition, some components support multiple UI states (e.g. focused, placeholder, readonly, etc.). React Aria components expose states using data attributes, which you can target in CSS selectors. For example: .react-aria-DateSegment[data-placeholder] { /* ... */ } .react-aria-DateSegment[data-readonly] { /* ... */ } .react-aria-DateSegment[data-placeholder] { /* ... */ } .react-aria-DateSegment[data-readonly] { /* ... */ } .react-aria-DateSegment[data-placeholder] { /* ... */ } .react-aria-DateSegment[data-readonly] { /* ... */ } The `className` and `style` props also accept functions which receive states for styling. This lets you dynamically determine the classes or styles to apply, which is useful when using utility CSS libraries like Tailwind. <DateSegment className={({ isPlaceholder }) => isPlaceholder ? 'bg-gray-300' : 'bg-gray-600'} /> <DateSegment className={({ isPlaceholder }) => isPlaceholder ? 'bg-gray-300' : 'bg-gray-600'} /> <DateSegment className={( { isPlaceholder } ) => isPlaceholder ? 'bg-gray-300' : 'bg-gray-600'} /> Render props may also be used as children to alter what elements are rendered based on the current state. For example, you could render the placeholder as a separate element to always reserve space for it. <DateSegment> {({ text, placeholder, isPlaceholder }) => ( <> <span style={{ visibility: isPlaceholder ? 'visible' : 'hidden' }}> {placeholder} </span> {isPlaceholder ? '' : text} </> )} </DateSegment> <DateSegment> {({ text, placeholder, isPlaceholder }) => ( <> <span style={{ visibility: isPlaceholder ? 'visible' : 'hidden' }} > {placeholder} </span> {isPlaceholder ? '' : text} </> )} </DateSegment> <DateSegment> {( { text, placeholder, isPlaceholder } ) => ( <> <span style={{ visibility: isPlaceholder ? 'visible' : 'hidden' }} > {placeholder} </span> {isPlaceholder ? '' : text} </> )} </DateSegment> The states, selectors, and render props for each component used in a `TimeField` are documented below. ### TimeField# A `TimeField` can be targeted with the `.react-aria-TimeField` CSS selector, or by overriding with a custom `className`. It supports the following states and render props: | Name | CSS Selector | Description | | --- | --- | --- | | `state` | `—` | State of the date field. | | `isInvalid` | `[data-invalid]` | Whether the date field is invalid. | | `isDisabled` | `[data-disabled]` | Whether the date field is disabled. | ### Label# A `Label` can be targeted with the `.react-aria-Label` CSS selector, or by overriding with a custom `className`. ### DateInput# A `DateInput` can be targeted with the `.react-aria-DateInput` CSS selector, or by overriding with a custom `className`. It supports the following states: | Name | CSS Selector | Description | | --- | --- | --- | | `isHovered` | `[data-hovered]` | Whether the date input is currently hovered with a mouse. | | `isFocusWithin` | `[data-focus-within]` | Whether an element within the date input is focused, either via a mouse or keyboard. | | `isFocusVisible` | `[data-focus-visible]` | Whether an element within the date input is keyboard focused. | | `isDisabled` | `[data-disabled]` | Whether the date input is disabled. | | `isInvalid` | `[data-invalid]` | Whether the date input is invalid. | ### DateSegment# A `DateSegment` can be targeted with the `.react-aria-DateSegment` CSS selector, or by overriding with a custom `className`. It supports the following states and render props: | Name | CSS Selector | Description | | --- | --- | --- | | `isHovered` | `[data-hovered]` | Whether the segment is currently hovered with a mouse. | | `isFocused` | `[data-focused]` | Whether the segment is focused, either via a mouse or keyboard. | | `isFocusVisible` | `[data-focus-visible]` | Whether the segment is keyboard focused. | | `isPlaceholder` | `[data-placeholder]` | Whether the value is a placeholder. | | `isReadOnly` | `[data-readonly]` | Whether the segment is read only. | | `isDisabled` | `[data-disabled]` | Whether the date field is disabled. | | `isInvalid` | `[data-invalid]` | Whether the date field is in an invalid state. | | `type` | `[data-type="..."]` | The type of segment. Values include `literal`, `year`, `month`, `day`, etc. | | `text` | `—` | The formatted text for the segment. | | `placeholder` | `—` | A placeholder string for the segment. | ### FieldError# A `FieldError` can be targeted with the `.react-aria-FieldError` CSS selector, or by overriding with a custom `className`. It supports the following render props: | Name | Description | | --- | --- | | `isInvalid` | Whether the input value is invalid. | | `validationErrors` | The current error messages for the input if it is invalid, otherwise an empty array. | | `validationDetails` | The native validation details for the input. | ## Advanced customization# * * * ### Composition# If you need to customize one of the components within a `TimeField`, such as `Label` or `DateSegment`, in many cases you can create a wrapper component. This lets you customize the props passed to the component. function MyDateSegment(props) { return <MyDateSegment {...props} className="my-date-segment" /> } function MyDateSegment(props) { return ( <MyDateSegment {...props} className="my-date-segment" /> ); } function MyDateSegment( props ) { return ( <MyDateSegment {...props} className="my-date-segment" /> ); } ### Contexts# All React Aria Components export a corresponding context that can be used to send props to them from a parent element. This enables you to build your own compositional APIs similar to those found in React Aria Components itself. You can send any prop or ref via context that you could pass to the corresponding component. The local props and ref on the component are merged with the ones passed via context, with the local props taking precedence (following the rules documented in mergeProps). | Component | Context | Props | Ref | | --- | --- | --- | --- | | `TimeField` | `TimeFieldContext` | ` TimeFieldProps ` | `HTMLDivElement` | This example shows a `FieldGroup` component that renders a group of time fields with a title and optional error message. It uses the useId hook to generate a unique id for the error message. All of the child TimeFields are marked invalid and associated with the error message via the `aria-describedby` attribute passed to the `TimeFieldContext` provider. import {TimeFieldContext} from 'react-aria-components'; import {useId} from 'react-aria'; interface FieldGroupProps { title?: string; children?: React.ReactNode; errorMessage?: string; } function FieldGroup({ title, children, errorMessage }: FieldGroupProps) { let errorId = useId(); return ( <fieldset> <legend>{title}</legend> <TimeFieldContext.Provider value={{ isInvalid: !!errorMessage, 'aria-describedby': errorMessage ? errorId : undefined }} > {children} </TimeFieldContext.Provider> {errorMessage && ( <small id={errorId} className="invalid">{errorMessage}</small> )} </fieldset> ); } <FieldGroup title="Schedule meeting time" errorMessage="End time must be after start time." > <MyTimeField label="Start" defaultValue={new Time(13)} /> <MyTimeField label="End" defaultValue={new Time(9)} /> </FieldGroup> import {TimeFieldContext} from 'react-aria-components'; import {useId} from 'react-aria'; interface FieldGroupProps { title?: string; children?: React.ReactNode; errorMessage?: string; } function FieldGroup( { title, children, errorMessage }: FieldGroupProps ) { let errorId = useId(); return ( <fieldset> <legend>{title}</legend> <TimeFieldContext.Provider value={{ isInvalid: !!errorMessage, 'aria-describedby': errorMessage ? errorId : undefined }} > {children} </TimeFieldContext.Provider> {errorMessage && ( <small id={errorId} className="invalid"> {errorMessage} </small> )} </fieldset> ); } <FieldGroup title="Schedule meeting time" errorMessage="End time must be after start time." > <MyTimeField label="Start" defaultValue={new Time(13)} /> <MyTimeField label="End" defaultValue={new Time(9)} /> </FieldGroup> import {TimeFieldContext} from 'react-aria-components'; import {useId} from 'react-aria'; interface FieldGroupProps { title?: string; children?: React.ReactNode; errorMessage?: string; } function FieldGroup( { title, children, errorMessage }: FieldGroupProps ) { let errorId = useId(); return ( <fieldset> <legend> {title} </legend> <TimeFieldContext.Provider value={{ isInvalid: !!errorMessage, 'aria-describedby': errorMessage ? errorId : undefined }} > {children} </TimeFieldContext.Provider> {errorMessage && ( <small id={errorId} className="invalid" > {errorMessage} </small> )} </fieldset> ); } <FieldGroup title="Schedule meeting time" errorMessage="End time must be after start time." > <MyTimeField label="Start" defaultValue={new Time( 13 )} /> <MyTimeField label="End" defaultValue={new Time( 9 )} /> </FieldGroup> Schedule meeting time Start 1:00 PM End 9:00 AM End time must be after start time. Show CSS fieldset { padding: 1.5em; width: fit-content; } .invalid { color: var(--invalid-color); margin-top: 1em; display: block; } fieldset { padding: 1.5em; width: fit-content; } .invalid { color: var(--invalid-color); margin-top: 1em; display: block; } fieldset { padding: 1.5em; width: fit-content; } .invalid { color: var(--invalid-color); margin-top: 1em; display: block; } ### Custom children# TimeField passes props to its child components, such as the label, via their associated contexts. These contexts are exported so you can also consume them in your own custom components. This enables you to reuse existing components from your app or component library together with React Aria Components. | Component | Context | Props | Ref | | --- | --- | --- | --- | | `Label` | `LabelContext` | ` LabelProps ` | `HTMLLabelElement` | | `Text` | `TextContext` | ` TextProps ` | `HTMLElement` | This example consumes from `LabelContext` in an existing styled label component to make it compatible with React Aria Components. The `useContextProps` hook merges the local props and ref with the ones provided via context by TimeField. import type {LabelProps} from 'react-aria-components'; import {LabelContext, useContextProps} from 'react-aria-components'; const MyCustomLabel = React.forwardRef( (props: LabelProps, ref: React.ForwardedRef<HTMLLabelElement>) => { // Merge the local props and ref with the ones provided via context. [props, ref] = useContextProps(props, ref, LabelContext); // ... your existing Label component return <label {...props} ref={ref} />; } ); import type {LabelProps} from 'react-aria-components'; import { LabelContext, useContextProps } from 'react-aria-components'; const MyCustomLabel = React.forwardRef( ( props: LabelProps, ref: React.ForwardedRef<HTMLLabelElement> ) => { // Merge the local props and ref with the ones provided via context. [props, ref] = useContextProps( props, ref, LabelContext ); // ... your existing Label component return <label {...props} ref={ref} />; } ); import type {LabelProps} from 'react-aria-components'; import { LabelContext, useContextProps } from 'react-aria-components'; const MyCustomLabel = React.forwardRef( ( props: LabelProps, ref: React.ForwardedRef< HTMLLabelElement > ) => { // Merge the local props and ref with the ones provided via context. [props, ref] = useContextProps( props, ref, LabelContext ); // ... your existing Label component return ( <label {...props} ref={ref} /> ); } ); Now you can use `MyCustomLabel` within a `TimeField`, in place of the builtin React Aria Components `Label`. <TimeField> <MyCustomLabel>Name</MyCustomLabel> <DateInput> {segment => <DateSegment segment={segment} />} </DateInput> </TimeField> <TimeField> <MyCustomLabel>Name</MyCustomLabel> <DateInput> {segment => <DateSegment segment={segment} />} </DateInput> </TimeField> <TimeField> <MyCustomLabel> Name </MyCustomLabel> <DateInput> {(segment) => ( <DateSegment segment={segment} /> )} </DateInput> </TimeField> ### State# TimeField provides a `TimeFieldState` object to its children via `TimeFieldStateContext`. This can be used to access and manipulate the time field's state. This example shows a `TimeZoneName` component that can be placed within a `TimeField` to display the full time zone name. import {TimeFieldStateContext} from 'react-aria-components'; import {useDateFormatter} from 'react-aria'; function TimeZoneName() { let state = React.useContext(TimeFieldStateContext)!; if ('timeZone' in state.value) { let formatter = useDateFormatter({ timeZoneName: 'long', timeZone: state.value.timeZone }); let timeZone = formatter .formatToParts(state.value.toDate()) .find((p) => p.type === 'timeZoneName').value; return <small>{timeZone}</small>; } return null; } <TimeField value={parseAbsoluteToLocal('2021-04-07T18:45:22Z')}> <Label>Time</Label> <DateInput> {(segment) => <DateSegment segment={segment} />} </DateInput> <TimeZoneName /></TimeField> import {TimeFieldStateContext} from 'react-aria-components'; import {useDateFormatter} from 'react-aria'; function TimeZoneName() { let state = React.useContext(TimeFieldStateContext)!; if ('timeZone' in state.value) { let formatter = useDateFormatter({ timeZoneName: 'long', timeZone: state.value.timeZone }); let timeZone = formatter .formatToParts(state.value.toDate()) .find((p) => p.type === 'timeZoneName').value; return <small>{timeZone}</small>; } return null; } <TimeField value={parseAbsoluteToLocal('2021-04-07T18:45:22Z')} > <Label>Time</Label> <DateInput> {(segment) => <DateSegment segment={segment} />} </DateInput> <TimeZoneName /></TimeField> import {TimeFieldStateContext} from 'react-aria-components'; import {useDateFormatter} from 'react-aria'; function TimeZoneName() { let state = React .useContext( TimeFieldStateContext )!; if ( 'timeZone' in state.value ) { let formatter = useDateFormatter({ timeZoneName: 'long', timeZone: state.value .timeZone }); let timeZone = formatter .formatToParts( state.value .toDate() ) .find((p) => p.type === 'timeZoneName' ).value; return ( <small> {timeZone} </small> ); } return null; } <TimeField value={parseAbsoluteToLocal( '2021-04-07T18:45:22Z' )} > <Label>Time</Label> <DateInput> {(segment) => ( <DateSegment segment={segment} /> )} </DateInput> <TimeZoneName /></TimeField> Time 6:45 PM UTC Coordinated Universal Time ### Hooks# If you need to customize things even further, such as accessing internal state or customizing DOM structure, you can drop down to the lower level Hook-based API. See useTimeField for more details. --- ## Page: https://react-spectrum.adobe.com/react-aria/DropZone.html # DropZone A drop zone is an area into which one or multiple objects can be dragged and dropped. <table><tbody><tr><th>install</th><td><code>yarn add react-aria-components</code></td></tr><tr><th>version</th><td>1.10.1</td></tr><tr><th>usage</th><td><code><span>import</span> {DropZone} <span>from</span> <span>'react-aria-components'</span></code></td></tr></tbody></table> View repository GitHub View package NPM ## Example# * * * import {DropZone, Text} from 'react-aria-components'; function Example() { let [dropped, setDropped] = React.useState(false); return ( <DropZone onDrop={() => { setDropped(true); }}> <Text slot="label"> {dropped ? "You dropped something" : "Drop object here"} </Text> </DropZone> ); } import {DropZone, Text} from 'react-aria-components'; function Example() { let [dropped, setDropped] = React.useState(false); return ( <DropZone onDrop={() => { setDropped(true); }} > <Text slot="label"> {dropped ? 'You dropped something' : 'Drop object here'} </Text> </DropZone> ); } import { DropZone, Text } from 'react-aria-components'; function Example() { let [ dropped, setDropped ] = React.useState( false ); return ( <DropZone onDrop={() => { setDropped(true); }} > <Text slot="label"> {dropped ? 'You dropped something' : 'Drop object here'} </Text> </DropZone> ); } Drop object here Show CSS @import "@react-aria/example-theme"; .react-aria-DropZone { color: var(--text-color); background: var(--overlay-background); border: 1px solid var(--border-color); forced-color-adjust: none; border-radius: 4px; appearance: none; vertical-align: middle; font-size: 1.2rem; text-align: center; margin: 0; outline: none; padding: 24px 12px; width: 25%; display: inline-block; &[data-focus-visible], &[data-drop-target] { outline: 2px solid var(--focus-ring-color); outline-offset: -1px; } &[data-drop-target] { background: var(--highlight-overlay); } } @import "@react-aria/example-theme"; .react-aria-DropZone { color: var(--text-color); background: var(--overlay-background); border: 1px solid var(--border-color); forced-color-adjust: none; border-radius: 4px; appearance: none; vertical-align: middle; font-size: 1.2rem; text-align: center; margin: 0; outline: none; padding: 24px 12px; width: 25%; display: inline-block; &[data-focus-visible], &[data-drop-target] { outline: 2px solid var(--focus-ring-color); outline-offset: -1px; } &[data-drop-target] { background: var(--highlight-overlay); } } @import "@react-aria/example-theme"; .react-aria-DropZone { color: var(--text-color); background: var(--overlay-background); border: 1px solid var(--border-color); forced-color-adjust: none; border-radius: 4px; appearance: none; vertical-align: middle; font-size: 1.2rem; text-align: center; margin: 0; outline: none; padding: 24px 12px; width: 25%; display: inline-block; &[data-focus-visible], &[data-drop-target] { outline: 2px solid var(--focus-ring-color); outline-offset: -1px; } &[data-drop-target] { background: var(--highlight-overlay); } } ## Features# * * * There is no native element to implement a drop zone in HTML. `DropZone` helps achieve accessible dropzone components that can be styled as needed. * **Styleable** – Hover, keyboard focus, and the drop target states are provided for easy styling. These styles only apply when interacting with an appropriate input device, unlike CSS pseudo classes. * **Accessible** – Support for native drag and drop via mouse and touch, as well as keyboard and screen reader interactions. Copy and paste is also supported as a keyboard accessible alternative. * **Flexible** – Files, directories, and custom data types can be dropped, and the contents of the drop zone can be fully customized. ## Anatomy# * * * A drop zone consists of a target element for the dropped objects. Users may drop objects via mouse, keyboard, or touch. `DropZone` accepts any content as its children, which may change when the user drops content. A FileTrigger is commonly paired with a DropZone to allow a user to choose files from their device. A visual label should be provided to `DropZone` using a `Text` element with a `label` slot. If it is not provided, then an `aria-label` or `aria-labelledby` prop must be passed to identify the visually hidden button to assistive technology. import {DropZone, Text} from 'react-aria-components'; <DropZone> <Text slot="label" /> </DropZone> import {DropZone, Text} from 'react-aria-components'; <DropZone> <Text slot="label" /> </DropZone> import { DropZone, Text } from 'react-aria-components'; <DropZone> <Text slot="label" /> </DropZone> ### Composed Components# A drop zone can include a `FileTrigger` as a child, which may also be used standalone or reused in other components. FileTrigger A file trigger allows a user to access the file system with any pressable component such as a Button. ## Events# * * * `DropZone` supports user interactions via mouse, keyboard, and touch. You can handle all of these via the `onDrop` prop. In addition, the `onDropEnter`, `onDropMove`, and `onDropExit` events are fired as the user interacts with the dropzone. import type {TextDropItem} from 'react-aria'; function Example() { let [dropped, setDropped] = React.useState(null); return ( <> <Draggable /> <DropZone onDrop={async (e) => { let items = await Promise.all( e.items .filter((item) => item.kind === 'text' && item.types.has('text/plain') ) .map((item: TextDropItem) => item.getText('text/plain')) ); setDropped(items.join('\n')); }} > <Text slot="label"> {dropped || 'Drop here'} </Text> </DropZone> </> ); } <Example /> import type {TextDropItem} from 'react-aria'; function Example() { let [dropped, setDropped] = React.useState(null); return ( <> <Draggable /> <DropZone onDrop={async (e) => { let items = await Promise.all( e.items .filter((item) => item.kind === 'text' && item.types.has('text/plain') ) .map((item: TextDropItem) => item.getText('text/plain') ) ); setDropped(items.join('\n')); }} > <Text slot="label"> {dropped || 'Drop here'} </Text> </DropZone> </> ); } <Example /> import type {TextDropItem} from 'react-aria'; function Example() { let [ dropped, setDropped ] = React.useState( null ); return ( <> <Draggable /> <DropZone onDrop={async ( e ) => { let items = await Promise .all( e.items .filter( (item) => item .kind === 'text' && item .types .has( 'text/plain' ) ) .map(( item: TextDropItem ) => item .getText( 'text/plain' ) ) ); setDropped( items.join( '\n' ) ); }} > <Text slot="label"> {dropped || 'Drop here'} </Text> </DropZone> </> ); } <Example /> Drag me Drop here The `Draggable` component used above is defined below. See useDrag for more details and documentation. Show code import {useDrag} from 'react-aria'; function Draggable() { let { dragProps, isDragging } = useDrag({ getItems() { return [{ 'text/plain': 'hello world', 'my-app-custom-type': JSON.stringify({ message: 'hello world' }) }]; } }); return ( <div {...dragProps} role="button" tabIndex={0} className={`draggable ${isDragging ? 'dragging' : ''}`} > Drag me </div> ); } import {useDrag} from 'react-aria'; function Draggable() { let { dragProps, isDragging } = useDrag({ getItems() { return [{ 'text/plain': 'hello world', 'my-app-custom-type': JSON.stringify({ message: 'hello world' }) }]; } }); return ( <div {...dragProps} role="button" tabIndex={0} className={`draggable ${ isDragging ? 'dragging' : '' }`} > Drag me </div> ); } import {useDrag} from 'react-aria'; function Draggable() { let { dragProps, isDragging } = useDrag({ getItems() { return [{ 'text/plain': 'hello world', 'my-app-custom-type': JSON.stringify( { message: 'hello world' } ) }]; } }); return ( <div {...dragProps} role="button" tabIndex={0} className={`draggable ${ isDragging ? 'dragging' : '' }`} > Drag me </div> ); } Show CSS .draggable { display: inline-block; vertical-align: top; border: 1px solid gray; padding: 10px; margin-right: 20px; border-radius: 4px; } .draggable.dragging { opacity: 0.5; } .draggable { display: inline-block; vertical-align: top; border: 1px solid gray; padding: 10px; margin-right: 20px; border-radius: 4px; } .draggable.dragging { opacity: 0.5; } .draggable { display: inline-block; vertical-align: top; border: 1px solid gray; padding: 10px; margin-right: 20px; border-radius: 4px; } .draggable.dragging { opacity: 0.5; } ## Labeling# * * * The `label` slot enables the user to reference the text content to define the dropzone's accessible name. import {Text} from 'react-aria-components'; function Example() { let [dropped, setDropped] = React.useState(false); return ( <DropZone onDrop={() => setDropped(true)}> <Text slot="label"> {dropped ? 'Successful drop!' : 'Drop files here'} </Text> </DropZone> ); } import {Text} from 'react-aria-components'; function Example() { let [dropped, setDropped] = React.useState(false); return ( <DropZone onDrop={() => setDropped(true)}> <Text slot="label"> {dropped ? 'Successful drop!' : 'Drop files here'} </Text> </DropZone> ); } import {Text} from 'react-aria-components'; function Example() { let [ dropped, setDropped ] = React.useState( false ); return ( <DropZone onDrop={() => setDropped(true)} > <Text slot="label"> {dropped ? 'Successful drop!' : 'Drop files here'} </Text> </DropZone> ); } Drop files here ## FileTrigger# * * * To allow the selection of files from the user's device, pass `FileTrigger` as a child of `DropZone`. import {Button, FileTrigger} from 'react-aria-components'; import type {FileDropItem} from 'react-aria'; function Example() { let [files, setFiles] = React.useState(null); return ( <DropZone onDrop={(e) => { let files = e.items.filter((file) => file.kind === 'file' ) as FileDropItem[]; let filenames = files.map((file) => file.name); setFiles(filenames.join(', ')); }} > <FileTrigger allowsMultiple onSelect={(e) => { let files = Array.from(e); let filenames = files.map((file) => file.name); setFiles(filenames.join(', ')); }} > <Button>Select files</Button> </FileTrigger> <Text slot="label" style={{ display: 'block' }}> {files || 'Drop files here'} </Text> </DropZone> ); } import {Button, FileTrigger} from 'react-aria-components'; import type {FileDropItem} from 'react-aria'; function Example() { let [files, setFiles] = React.useState(null); return ( <DropZone onDrop={(e) => { let files = e.items.filter((file) => file.kind === 'file' ) as FileDropItem[]; let filenames = files.map((file) => file.name); setFiles(filenames.join(', ')); }} > <FileTrigger allowsMultiple onSelect={(e) => { let files = Array.from(e); let filenames = files.map((file) => file.name); setFiles(filenames.join(', ')); }} > <Button>Select files</Button> </FileTrigger> <Text slot="label" style={{ display: 'block' }}> {files || 'Drop files here'} </Text> </DropZone> ); } import { Button, FileTrigger } from 'react-aria-components'; import type {FileDropItem} from 'react-aria'; function Example() { let [files, setFiles] = React.useState(null); return ( <DropZone onDrop={(e) => { let files = e .items.filter(( file ) => file.kind === 'file' ) as FileDropItem[]; let filenames = files.map(( file ) => file.name ); setFiles( filenames.join( ', ' ) ); }} > <FileTrigger allowsMultiple onSelect={( e ) => { let files = Array.from( e ); let filenames = files.map(( file ) => file.name ); setFiles( filenames .join(', ') ); }} > <Button> Select files </Button> </FileTrigger> <Text slot="label" style={{ display: 'block' }} > {files || 'Drop files here'} </Text> </DropZone> ); } Select filesDrop files here ## Visual feedback# * * * A dropzone displays visual feedback to the user when a drag hovers over the drop target by passing the `getDropOperation` function. If a drop target only supports data of specific types (e.g. images, videos, text, etc.), then it should implement the `getDropOperation` prop and return `cancel` for types that aren't supported. This will prevent visual feedback indicating that the drop target accepts the dragged data when this is not true. Read more about getDropOperation. function Example() { let [dropped, setDropped] = React.useState(false); return ( <DropZone getDropOperation={(types) => types.has('image/png') ? 'copy' : 'cancel'} onDrop={() => setDropped(true)}> {dropped ? 'Successful drop!' : 'Drop files here'} </DropZone> ); } function Example() { let [dropped, setDropped] = React.useState(false); return ( <DropZone getDropOperation={(types) => types.has('image/png') ? 'copy' : 'cancel'} onDrop={() => setDropped(true)} > {dropped ? 'Successful drop!' : 'Drop files here'} </DropZone> ); } function Example() { let [ dropped, setDropped ] = React.useState( false ); return ( <DropZone getDropOperation={( types ) => types.has( 'image/png' ) ? 'copy' : 'cancel'} onDrop={() => setDropped(true)} > {dropped ? 'Successful drop!' : 'Drop files here'} </DropZone> ); } Drop files here ## Props# * * * | Name | Type | Description | | --- | --- | --- | | `getDropOperation` | `( (types: DragTypes , , allowedOperations: DropOperation [] )) => DropOperation ` | A function returning the drop operation to be performed when items matching the given types are dropped on the drop target. | | `isDisabled` | `boolean` | Whether the drop target is disabled. If true, the drop target will not accept any drops. | | `children` | `ReactNode | ( (values: DropZoneRenderProps & & { defaultChildren: ReactNode | | undefined } )) => ReactNode` | The children of the component. A function may be provided to alter the children based on component state. | | `className` | `string | ( (values: DropZoneRenderProps & & { defaultClassName: string | | undefined } )) => string` | The CSS className for the element. A function may be provided to compute the class based on component state. | | `style` | `CSSProperties | ( (values: DropZoneRenderProps & & { defaultStyle: CSSProperties } )) => CSSProperties | undefined` | The inline style for the element. A function may be provided to compute the style based on component state. | Events | Name | Type | Description | | --- | --- | --- | | `onDropEnter` | `( (e: DropEnterEvent )) => void` | Handler that is called when a valid drag enters the drop target. | | `onDropMove` | `( (e: DropMoveEvent )) => void` | Handler that is called when a valid drag is moved within the drop target. | | `onDropActivate` | `( (e: DropActivateEvent )) => void` | Handler that is called after a valid drag is held over the drop target for a period of time. This typically opens the item so that the user can drop within it. | | `onDropExit` | `( (e: DropExitEvent )) => void` | Handler that is called when a valid drag exits the drop target. | | `onDrop` | `( (e: DropEvent )) => void` | Handler that is called when a valid drag is dropped on the drop target. | | `onHoverStart` | `( (e: HoverEvent )) => void` | Handler that is called when a hover interaction starts. | | `onHoverEnd` | `( (e: HoverEvent )) => void` | Handler that is called when a hover interaction ends. | | `onHoverChange` | `( (isHovering: boolean )) => void` | Handler that is called when the hover state changes. | Layout | Name | Type | Description | | --- | --- | --- | | `slot` | `string | null` | A slot name for the component. Slots allow the component to receive props from a parent component. An explicit `null` value indicates that the local props completely override all props received from a parent. | Accessibility | Name | Type | Description | | --- | --- | --- | | `aria-label` | `string` | Defines a string value that labels the current element. | | `aria-labelledby` | `string` | Identifies the element (or elements) that labels the current element. | | `aria-describedby` | `string` | Identifies the element (or elements) that describes the object. | | `aria-details` | `string` | Identifies the element (or elements) that provide a detailed, extended description for the object. | ## Styling# * * * React Aria components can be styled in many ways, including using CSS classes, inline styles, utility classes (e.g. Tailwind), CSS-in-JS (e.g. Styled Components), etc. By default, all components include a builtin `className` attribute which can be targeted using CSS selectors. These follow the `react-aria-ComponentName` naming convention. .react-aria-DropZone { /* ... */ } .react-aria-DropZone { /* ... */ } .react-aria-DropZone { /* ... */ } A custom `className` can also be specified on any component. This overrides the default `className` provided by React Aria with your own. <DropZone className="my-dropzone"> {/* ... */} </DropZone> <DropZone className="my-dropzone"> {/* ... */} </DropZone> <DropZone className="my-dropzone"> {/* ... */} </DropZone> In addition, some components support multiple UI states (e.g. focused, placeholder, readonly, etc.). React Aria components expose states using data attributes, which you can target in CSS selectors. For example: .react-aria-DropZone[data-drop-target] { /* ... */ } .react-aria-DropZone[data-drop-target] { /* ... */ } .react-aria-DropZone[data-drop-target] { /* ... */ } The `className` and `style` props also accept functions which receive states for styling. This lets you dynamically determine the classes or styles to apply, which is useful when using utility CSS libraries like Tailwind. <DropZone className={({ isDropTarget }) => isDropTarget ? 'bg-gray-700' : 'bg-gray-600'} /> <DropZone className={({ isDropTarget }) => isDropTarget ? 'bg-gray-700' : 'bg-gray-600'} /> <DropZone className={( { isDropTarget } ) => isDropTarget ? 'bg-gray-700' : 'bg-gray-600'} /> Render props may also be used as children to alter what elements are rendered based on the current state. For example, you could render an extra element when the drop target is in an active state. <DropZone> {({isDropTarget}) => ( <> {isDropTarget && <DropHighlight/>} Drop item here </> )} </DropZone> <DropZone> {({isDropTarget}) => ( <> {isDropTarget && <DropHighlight/>} Drop item here </> )} </DropZone> <DropZone> {( { isDropTarget } ) => ( <> {isDropTarget && ( <DropHighlight /> )} Drop item here </> )} </DropZone> The states, selectors, and render props for `DropZone` are documented below. | Name | CSS Selector | Description | | --- | --- | --- | | `isHovered` | `[data-hovered]` | Whether the dropzone is currently hovered with a mouse. | | `isFocused` | `[data-focused]` | Whether the dropzone is focused, either via a mouse or keyboard. | | `isFocusVisible` | `[data-focus-visible]` | Whether the dropzone is keyboard focused. | | `isDropTarget` | `[data-drop-target]` | Whether the dropzone is the drop target. | | `isDisabled` | `[data-disabled]` | Whether the dropzone is disabled. | ## Advanced customization# * * * ### Hooks# If you need to customize things further, such as accessing internal state or customizing DOM structure, you can drop down to the lower level Hook-based API. See useDrop for more details. --- ## Page: https://react-spectrum.adobe.com/react-aria/Checkbox.html # Checkbox A checkbox allows a user to select multiple items from a list of individual items, or to mark one individual item as selected. <table><tbody><tr><th>install</th><td><code>yarn add react-aria-components</code></td></tr><tr><th>version</th><td>1.10.1</td></tr><tr><th>usage</th><td><code><span>import</span> {Checkbox} <span>from</span> <span>'react-aria-components'</span></code></td></tr></tbody></table> View ARIA pattern W3C View repository GitHub View package NPM ## Example# * * * import {Checkbox} from 'react-aria-components'; <Checkbox> <div className="checkbox"> <svg viewBox="0 0 18 18" aria-hidden="true"> <polyline points="1 9 7 14 15 4" /> </svg> </div> Unsubscribe </Checkbox> import {Checkbox} from 'react-aria-components'; <Checkbox> <div className="checkbox"> <svg viewBox="0 0 18 18" aria-hidden="true"> <polyline points="1 9 7 14 15 4" /> </svg> </div> Unsubscribe </Checkbox> import {Checkbox} from 'react-aria-components'; <Checkbox> <div className="checkbox"> <svg viewBox="0 0 18 18" aria-hidden="true" > <polyline points="1 9 7 14 15 4" /> </svg> </div> Unsubscribe </Checkbox> Unsubscribe Show CSS @import "@react-aria/example-theme"; .react-aria-Checkbox { --selected-color: var(--highlight-background); --selected-color-pressed: var(--highlight-background-pressed); --checkmark-color: var(--highlight-foreground); display: flex; /* This is needed so the HiddenInput is positioned correctly */ position: relative; align-items: center; gap: 0.571rem; font-size: 1.143rem; color: var(--text-color); forced-color-adjust: none; .checkbox { width: 1.143rem; height: 1.143rem; border: 2px solid var(--border-color); border-radius: 4px; transition: all 200ms; display: flex; align-items: center; justify-content: center; flex-shrink: 0; } svg { width: 1rem; height: 1rem; fill: none; stroke: var(--checkmark-color); stroke-width: 3px; stroke-dasharray: 22px; stroke-dashoffset: 66; transition: all 200ms; } &[data-pressed] .checkbox { border-color: var(--border-color-pressed); } &[data-focus-visible] .checkbox { outline: 2px solid var(--focus-ring-color); outline-offset: 2px; } &[data-selected], &[data-indeterminate] { .checkbox { border-color: var(--selected-color); background: var(--selected-color); } &[data-pressed] .checkbox { border-color: var(--selected-color-pressed); background: var(--selected-color-pressed); } svg { stroke-dashoffset: 44; } } &[data-indeterminate] { & svg { stroke: none; fill: var(--checkmark-color); } } } @import "@react-aria/example-theme"; .react-aria-Checkbox { --selected-color: var(--highlight-background); --selected-color-pressed: var(--highlight-background-pressed); --checkmark-color: var(--highlight-foreground); display: flex; /* This is needed so the HiddenInput is positioned correctly */ position: relative; align-items: center; gap: 0.571rem; font-size: 1.143rem; color: var(--text-color); forced-color-adjust: none; .checkbox { width: 1.143rem; height: 1.143rem; border: 2px solid var(--border-color); border-radius: 4px; transition: all 200ms; display: flex; align-items: center; justify-content: center; flex-shrink: 0; } svg { width: 1rem; height: 1rem; fill: none; stroke: var(--checkmark-color); stroke-width: 3px; stroke-dasharray: 22px; stroke-dashoffset: 66; transition: all 200ms; } &[data-pressed] .checkbox { border-color: var(--border-color-pressed); } &[data-focus-visible] .checkbox { outline: 2px solid var(--focus-ring-color); outline-offset: 2px; } &[data-selected], &[data-indeterminate] { .checkbox { border-color: var(--selected-color); background: var(--selected-color); } &[data-pressed] .checkbox { border-color: var(--selected-color-pressed); background: var(--selected-color-pressed); } svg { stroke-dashoffset: 44; } } &[data-indeterminate] { & svg { stroke: none; fill: var(--checkmark-color); } } } @import "@react-aria/example-theme"; .react-aria-Checkbox { --selected-color: var(--highlight-background); --selected-color-pressed: var(--highlight-background-pressed); --checkmark-color: var(--highlight-foreground); display: flex; /* This is needed so the HiddenInput is positioned correctly */ position: relative; align-items: center; gap: 0.571rem; font-size: 1.143rem; color: var(--text-color); forced-color-adjust: none; .checkbox { width: 1.143rem; height: 1.143rem; border: 2px solid var(--border-color); border-radius: 4px; transition: all 200ms; display: flex; align-items: center; justify-content: center; flex-shrink: 0; } svg { width: 1rem; height: 1rem; fill: none; stroke: var(--checkmark-color); stroke-width: 3px; stroke-dasharray: 22px; stroke-dashoffset: 66; transition: all 200ms; } &[data-pressed] .checkbox { border-color: var(--border-color-pressed); } &[data-focus-visible] .checkbox { outline: 2px solid var(--focus-ring-color); outline-offset: 2px; } &[data-selected], &[data-indeterminate] { .checkbox { border-color: var(--selected-color); background: var(--selected-color); } &[data-pressed] .checkbox { border-color: var(--selected-color-pressed); background: var(--selected-color-pressed); } svg { stroke-dashoffset: 44; } } &[data-indeterminate] { & svg { stroke: none; fill: var(--checkmark-color); } } } ## Features# * * * Checkboxes can be built with the <input> HTML element, but this can be difficult to style. `Checkbox` helps achieve accessible checkboxes that can be styled as needed. * **Styleable** – Hover, press, keyboard focus, selection, and indeterminate states are provided for easy styling. These states only apply when interacting with an appropriate input device, unlike CSS pseudo classes. * **Accessible** – Uses a visually hidden `<input>` element under the hood, which also enables HTML form integration and autofill. A label element is built-in to ensure the checkbox is usable with assistive technologies. * **Cross-browser** – Mouse, touch, keyboard, and focus interactions are normalized to ensure consistency across browsers and devices. ## Anatomy# * * * A checkbox consists of a visual selection indicator and a label. Checkboxes support three selection states: checked, unchecked, and indeterminate. Users may click or touch a checkbox to toggle the selection state, or use the Tab key to navigate to it and the Space key to toggle it. In most cases, checkboxes should have a visual label. If the checkbox does not have a visible label, an `aria-label` or `aria-labelledby` prop must be passed instead to identify the element to assistive technology. ## Starter kits# * * * To help kick-start your project, we offer starter kits that include example implementations of all React Aria components with various styling solutions. All components are fully styled, including support for dark mode, high contrast mode, and all UI states. Each starter comes with a pre-configured Storybook that you can experiment with, or use as a starting point for your own component library. Vanilla CSS Download ZIP Preview Tailwind CSS Download ZIP Preview ## Reusable wrappers# * * * If you will use a Checkbox in multiple places in your app, you can wrap all of the pieces into a reusable component. This way, the DOM structure, styling code, and other logic are defined in a single place and reused everywhere to ensure consistency. This example wraps `Checkbox` and all of its children together into a single component. It also shows how to use render props to conditionally render a different indicator icon when the checkbox is in an indeterminate state. import type {CheckboxProps} from 'react-aria-components'; export function MyCheckbox( { children, ...props }: Omit<CheckboxProps, 'children'> & { children?: React.ReactNode; } ) { return ( <Checkbox {...props}> {({ isIndeterminate }) => ( <> <div className="checkbox"> <svg viewBox="0 0 18 18" aria-hidden="true"> {isIndeterminate ? <rect x={1} y={7.5} width={15} height={3} /> : <polyline points="1 9 7 14 15 4" />} </svg> </div> {children} </> )} </Checkbox> ); } <MyCheckbox>Unsubscribe</MyCheckbox> import type {CheckboxProps} from 'react-aria-components'; export function MyCheckbox( { children, ...props }: & Omit<CheckboxProps, 'children'> & { children?: React.ReactNode } ) { return ( <Checkbox {...props}> {({ isIndeterminate }) => ( <> <div className="checkbox"> <svg viewBox="0 0 18 18" aria-hidden="true"> {isIndeterminate ? ( <rect x={1} y={7.5} width={15} height={3} /> ) : <polyline points="1 9 7 14 15 4" />} </svg> </div> {children} </> )} </Checkbox> ); } <MyCheckbox>Unsubscribe</MyCheckbox> import type {CheckboxProps} from 'react-aria-components'; export function MyCheckbox( { children, ...props }: & Omit< CheckboxProps, 'children' > & { children?: React.ReactNode; } ) { return ( <Checkbox {...props}> {( { isIndeterminate } ) => ( <> <div className="checkbox"> <svg viewBox="0 0 18 18" aria-hidden="true" > {isIndeterminate ? ( <rect x={1} y={7.5} width={15} height={3} /> ) : ( <polyline points="1 9 7 14 15 4" /> )} </svg> </div> {children} </> )} </Checkbox> ); } <MyCheckbox> Unsubscribe </MyCheckbox> Unsubscribe ## Value# * * * ### Default value# Checkboxes are not selected by default. The `defaultSelected` prop can be used to set the default state. <MyCheckbox defaultSelected>Subscribe</MyCheckbox> <MyCheckbox defaultSelected>Subscribe</MyCheckbox> <MyCheckbox defaultSelected > Subscribe </MyCheckbox> Subscribe ### Controlled value# The `isSelected` prop can be used to make the selected state controlled. The `onChange` event is fired when the user presses the checkbox, and receives the new value. function Example() { let [selected, setSelection] = React.useState(false); return ( <> <MyCheckbox isSelected={selected} onChange={setSelection}> Subscribe </MyCheckbox> <p>{`You are ${selected ? 'subscribed' : 'unsubscribed'}`}</p> </> ); } function Example() { let [selected, setSelection] = React.useState(false); return ( <> <MyCheckbox isSelected={selected} onChange={setSelection} > Subscribe </MyCheckbox> <p> {`You are ${ selected ? 'subscribed' : 'unsubscribed' }`} </p> </> ); } function Example() { let [ selected, setSelection ] = React.useState( false ); return ( <> <MyCheckbox isSelected={selected} onChange={setSelection} > Subscribe </MyCheckbox> <p> {`You are ${ selected ? 'subscribed' : 'unsubscribed' }`} </p> </> ); } Subscribe You are unsubscribed ### Indeterminate# A Checkbox can be in an indeterminate state, controlled using the `isIndeterminate` prop. This overrides the appearance of the Checkbox, whether selection is controlled or uncontrolled. The Checkbox will visually remain indeterminate until the `isIndeterminate` prop is set to false, regardless of user interaction. <MyCheckbox isIndeterminate>Subscribe</MyCheckbox> <MyCheckbox isIndeterminate>Subscribe</MyCheckbox> <MyCheckbox isIndeterminate > Subscribe </MyCheckbox> Subscribe ### HTML forms# Checkbox supports the `name` and `value` props for integration with HTML forms. <MyCheckbox name="newsletter" value="subscribe">Subscribe</MyCheckbox> <MyCheckbox name="newsletter" value="subscribe"> Subscribe </MyCheckbox> <MyCheckbox name="newsletter" value="subscribe" > Subscribe </MyCheckbox> Subscribe ## Validation# * * * Checkboxes can display a validation state to communicate to the user if the current value is invalid. Implement your own validation logic in your app and set the `isInvalid` prop accordingly. <MyCheckbox isInvalid>I accept the terms and conditions</MyCheckbox> <MyCheckbox isInvalid> I accept the terms and conditions </MyCheckbox> <MyCheckbox isInvalid> I accept the terms and conditions </MyCheckbox> I accept the terms and conditions Show CSS .react-aria-Checkbox { &[data-invalid] { .checkbox { --checkmark-color: var(--gray-50); border-color: var(--invalid-color); } &[data-pressed] .checkbox { border-color: var(--invalid-color-pressed); } &[data-selected], &[data-indeterminate] { .checkbox { background: var(--invalid-color); } &[data-pressed] .checkbox { background: var(--invalid-color-pressed); } } } } .react-aria-Checkbox { &[data-invalid] { .checkbox { --checkmark-color: var(--gray-50); border-color: var(--invalid-color); } &[data-pressed] .checkbox { border-color: var(--invalid-color-pressed); } &[data-selected], &[data-indeterminate] { .checkbox { background: var(--invalid-color); } &[data-pressed] .checkbox { background: var(--invalid-color-pressed); } } } } .react-aria-Checkbox { &[data-invalid] { .checkbox { --checkmark-color: var(--gray-50); border-color: var(--invalid-color); } &[data-pressed] .checkbox { border-color: var(--invalid-color-pressed); } &[data-selected], &[data-indeterminate] { .checkbox { background: var(--invalid-color); } &[data-pressed] .checkbox { background: var(--invalid-color-pressed); } } } } ## Disabled# * * * Checkboxes can be disabled using the `isDisabled` prop. <MyCheckbox isDisabled>Subscribe</MyCheckbox> <MyCheckbox isDisabled>Subscribe</MyCheckbox> <MyCheckbox isDisabled> Subscribe </MyCheckbox> Subscribe Show CSS .react-aria-Checkbox { &[data-disabled] { color: var(--text-color-disabled); .checkbox { border-color: var(--border-color-disabled); } } } .react-aria-Checkbox { &[data-disabled] { color: var(--text-color-disabled); .checkbox { border-color: var(--border-color-disabled); } } } .react-aria-Checkbox { &[data-disabled] { color: var(--text-color-disabled); .checkbox { border-color: var(--border-color-disabled); } } } ### Read only# The `isReadOnly` prop makes the selection immutable. Unlike `isDisabled`, the Checkbox remains focusable. See the MDN docs for more information. <MyCheckbox isSelected isReadOnly>Agree</MyCheckbox> <MyCheckbox isSelected isReadOnly>Agree</MyCheckbox> <MyCheckbox isSelected isReadOnly > Agree </MyCheckbox> Agree ## Props# * * * | Name | Type | Default | Description | | --- | --- | --- | --- | | `inputRef` | ` RefObject <HTMLInputElement | null>` | — | A ref for the HTML input element. | | `isIndeterminate` | `boolean` | — | Indeterminism is presentational only. The indeterminate visual representation remains regardless of user interaction. | | `value` | `string` | — | The value of the input element, used when submitting an HTML form. See MDN. | | `defaultSelected` | `boolean` | — | Whether the element should be selected (uncontrolled). | | `isSelected` | `boolean` | — | Whether the element should be selected (controlled). | | `isDisabled` | `boolean` | — | Whether the input is disabled. | | `isReadOnly` | `boolean` | — | Whether the input can be selected but not changed by the user. | | `isRequired` | `boolean` | — | Whether user input is required on the input before form submission. | | `isInvalid` | `boolean` | — | Whether the input value is invalid. | | `validate` | `( (value: boolean )) => ValidationError | true | null | undefined` | — | A function that returns an error message if a given value is invalid. Validation errors are displayed to the user when the form is submitted if `validationBehavior="native"`. For realtime validation, use the `isInvalid` prop instead. | | `autoFocus` | `boolean` | — | Whether the element should receive focus on render. | | `name` | `string` | — | The name of the input element, used when submitting an HTML form. See MDN. | | `validationBehavior` | `'native' | 'aria'` | `'native'` | Whether to use native HTML form validation to prevent form submission when the value is missing or invalid, or mark the field as required or invalid via ARIA. | | `children` | `ReactNode | ( (values: CheckboxRenderProps & & { defaultChildren: ReactNode | | undefined } )) => ReactNode` | — | The children of the component. A function may be provided to alter the children based on component state. | | `className` | `string | ( (values: CheckboxRenderProps & & { defaultClassName: string | | undefined } )) => string` | — | The CSS className for the element. A function may be provided to compute the class based on component state. | | `style` | `CSSProperties | ( (values: CheckboxRenderProps & & { defaultStyle: CSSProperties } )) => CSSProperties | undefined` | — | The inline style for the element. A function may be provided to compute the style based on component state. | Events | Name | Type | Description | | --- | --- | --- | | `onChange` | `( (isSelected: boolean )) => void` | Handler that is called when the element's selection state changes. | | `onFocus` | `( (e: FocusEvent<Target> )) => void` | Handler that is called when the element receives focus. | | `onBlur` | `( (e: FocusEvent<Target> )) => void` | Handler that is called when the element loses focus. | | `onFocusChange` | `( (isFocused: boolean )) => void` | Handler that is called when the element's focus status changes. | | `onKeyDown` | `( (e: KeyboardEvent )) => void` | Handler that is called when a key is pressed. | | `onKeyUp` | `( (e: KeyboardEvent )) => void` | Handler that is called when a key is released. | | `onHoverStart` | `( (e: HoverEvent )) => void` | Handler that is called when a hover interaction starts. | | `onHoverEnd` | `( (e: HoverEvent )) => void` | Handler that is called when a hover interaction ends. | | `onHoverChange` | `( (isHovering: boolean )) => void` | Handler that is called when the hover state changes. | Layout | Name | Type | Description | | --- | --- | --- | | `slot` | `string | null` | A slot name for the component. Slots allow the component to receive props from a parent component. An explicit `null` value indicates that the local props completely override all props received from a parent. | Accessibility | Name | Type | Description | | --- | --- | --- | | `id` | `string` | The element's unique identifier. See MDN. | | `excludeFromTabOrder` | `boolean` | Whether to exclude the element from the sequential tab order. If true, the element will not be focusable via the keyboard by tabbing. This should be avoided except in rare scenarios where an alternative means of accessing the element or its functionality via the keyboard is available. | | `aria-controls` | `string` | Identifies the element (or elements) whose contents or presence are controlled by the current element. | | `aria-label` | `string` | Defines a string value that labels the current element. | | `aria-labelledby` | `string` | Identifies the element (or elements) that labels the current element. | | `aria-describedby` | `string` | Identifies the element (or elements) that describes the object. | | `aria-details` | `string` | Identifies the element (or elements) that provide a detailed, extended description for the object. | | `aria-errormessage` | `string` | Identifies the element that provides an error message for the object. | ## Styling# * * * React Aria components can be styled in many ways, including using CSS classes, inline styles, utility classes (e.g. Tailwind), CSS-in-JS (e.g. Styled Components), etc. By default, all components include a builtin `className` attribute which can be targeted using CSS selectors. These follow the `react-aria-ComponentName` naming convention. .react-aria-Checkbox { /* ... */ } .react-aria-Checkbox { /* ... */ } .react-aria-Checkbox { /* ... */ } A custom `className` can also be specified on any component. This overrides the default `className` provided by React Aria with your own. <Checkbox className="my-checkbox"> {/* ... */} </Checkbox> <Checkbox className="my-checkbox"> {/* ... */} </Checkbox> <Checkbox className="my-checkbox"> {/* ... */} </Checkbox> In addition, some components support multiple UI states (e.g. focused, placeholder, readonly, etc.). React Aria components expose states using data attributes, which you can target in CSS selectors. For example: .react-aria-Checkbox[data-pressed] { /* ... */ } .react-aria-Checkbox[data-pressed] { /* ... */ } .react-aria-Checkbox[data-pressed] { /* ... */ } The `className` and `style` props also accept functions which receive states for styling. This lets you dynamically determine the classes or styles to apply, which is useful when using utility CSS libraries like Tailwind. <Checkbox className={({ isPressed }) => isPressed ? 'bg-gray-700' : 'bg-gray-600'} /> <Checkbox className={({ isPressed }) => isPressed ? 'bg-gray-700' : 'bg-gray-600'} /> <Checkbox className={( { isPressed } ) => isPressed ? 'bg-gray-700' : 'bg-gray-600'} /> Render props may also be used as children to alter what elements are rendered based on the current state. For example, you could render an extra element when the checkbox is selected. <Checkbox> {({isSelected}) => ( <> {isSelected && <CheckIcon />} Subscribe </> )} </Checkbox> <Checkbox> {({isSelected}) => ( <> {isSelected && <CheckIcon />} Subscribe </> )} </Checkbox> <Checkbox> {( { isSelected } ) => ( <> {isSelected && ( <CheckIcon /> )} Subscribe </> )} </Checkbox> The states, selectors, and render props for `Checkbox` are documented below. | Name | CSS Selector | Description | | --- | --- | --- | | `isSelected` | `[data-selected]` | Whether the checkbox is selected. | | `isIndeterminate` | `[data-indeterminate]` | Whether the checkbox is indeterminate. | | `isHovered` | `[data-hovered]` | Whether the checkbox is currently hovered with a mouse. | | `isPressed` | `[data-pressed]` | Whether the checkbox is currently in a pressed state. | | `isFocused` | `[data-focused]` | Whether the checkbox is focused, either via a mouse or keyboard. | | `isFocusVisible` | `[data-focus-visible]` | Whether the checkbox is keyboard focused. | | `isDisabled` | `[data-disabled]` | Whether the checkbox is disabled. | | `isReadOnly` | `[data-readonly]` | Whether the checkbox is read only. | | `isInvalid` | `[data-invalid]` | Whether the checkbox invalid. | | `isRequired` | `[data-required]` | Whether the checkbox is required. | ## Advanced customization# * * * ### Contexts# All React Aria Components export a corresponding context that can be used to send props to them from a parent element. This enables you to build your own compositional APIs similar to those found in React Aria Components itself. You can send any prop or ref via context that you could pass to the corresponding component. The local props and ref on the component are merged with the ones passed via context, with the local props taking precedence (following the rules documented in mergeProps). | Component | Context | Props | Ref | | --- | --- | --- | --- | | `Checkbox` | `CheckboxContext` | ` CheckboxProps ` | `HTMLLabelElement` | This example shows a `CheckboxDescription` component that accepts a checkbox in its children and renders a description element below it. It uses the useId hook to generate a unique id for the description, and associates it with the checkbox via the `aria-describedby` attribute passed to the `CheckboxContext` provider. import {CheckboxContext} from 'react-aria-components'; import {useId} from 'react-aria'; interface CheckboxDescriptionProps { children?: React.ReactNode; description?: string; } function CheckboxDescription( { children, description }: CheckboxDescriptionProps ) { let descriptionId = useId(); return ( <div> <CheckboxContext.Provider value={{ 'aria-describedby': descriptionId }}> {children} </CheckboxContext.Provider> <small id={descriptionId}>{description}</small> </div> ); } <CheckboxDescription description="You will receive our newsletter once per week. Unsubscribe at any time."> <MyCheckbox defaultSelected>Subscribe</MyCheckbox> </CheckboxDescription> import {CheckboxContext} from 'react-aria-components'; import {useId} from 'react-aria'; interface CheckboxDescriptionProps { children?: React.ReactNode; description?: string; } function CheckboxDescription( { children, description }: CheckboxDescriptionProps ) { let descriptionId = useId(); return ( <div> <CheckboxContext.Provider value={{ 'aria-describedby': descriptionId }} > {children} </CheckboxContext.Provider> <small id={descriptionId}>{description}</small> </div> ); } <CheckboxDescription description="You will receive our newsletter once per week. Unsubscribe at any time."> <MyCheckbox defaultSelected>Subscribe</MyCheckbox> </CheckboxDescription> import {CheckboxContext} from 'react-aria-components'; import {useId} from 'react-aria'; interface CheckboxDescriptionProps { children?: React.ReactNode; description?: string; } function CheckboxDescription( { children, description }: CheckboxDescriptionProps ) { let descriptionId = useId(); return ( <div> <CheckboxContext.Provider value={{ 'aria-describedby': descriptionId }} > {children} </CheckboxContext.Provider> <small id={descriptionId} > {description} </small> </div> ); } <CheckboxDescription description="You will receive our newsletter once per week. Unsubscribe at any time."> <MyCheckbox defaultSelected > Subscribe </MyCheckbox> </CheckboxDescription> SubscribeYou will receive our newsletter once per week. Unsubscribe at any time. ### Hooks# If you need to customize things further, such as intercepting events or customizing DOM structure, you can drop down to the lower level Hook-based API. Consume from `CheckboxContext` in your component with `useContextProps` to make it compatible with other React Aria Components. See useCheckbox for more details. import type {CheckboxProps} from 'react-aria-components'; import {CheckboxContext, useContextProps} from 'react-aria-components'; import {useToggleState} from 'react-stately'; import {useCheckbox} from 'react-aria'; const MyCheckbox = React.forwardRef( (props: CheckboxProps, ref: React.ForwardedRef<HTMLInputElement>) => { // Merge the local props and ref with the ones provided via context. [props, ref] = useContextProps(props, ref, CheckboxContext); let state = useToggleState(props); let { inputProps } = useCheckbox(props, state, ref); return <input {...inputProps} ref={ref} />; } ); import type {CheckboxProps} from 'react-aria-components'; import { CheckboxContext, useContextProps } from 'react-aria-components'; import {useToggleState} from 'react-stately'; import {useCheckbox} from 'react-aria'; const MyCheckbox = React.forwardRef( ( props: CheckboxProps, ref: React.ForwardedRef<HTMLInputElement> ) => { // Merge the local props and ref with the ones provided via context. [props, ref] = useContextProps( props, ref, CheckboxContext ); let state = useToggleState(props); let { inputProps } = useCheckbox(props, state, ref); return <input {...inputProps} ref={ref} />; } ); import type {CheckboxProps} from 'react-aria-components'; import { CheckboxContext, useContextProps } from 'react-aria-components'; import {useToggleState} from 'react-stately'; import {useCheckbox} from 'react-aria'; const MyCheckbox = React .forwardRef( ( props: CheckboxProps, ref: React.ForwardedRef< HTMLInputElement > ) => { // Merge the local props and ref with the ones provided via context. [props, ref] = useContextProps( props, ref, CheckboxContext ); let state = useToggleState( props ); let { inputProps } = useCheckbox( props, state, ref ); return ( <input {...inputProps} ref={ref} /> ); } ); --- ## Page: https://react-spectrum.adobe.com/react-aria/CheckboxGroup.html # CheckboxGroup A checkbox group allows a user to select multiple items from a list of options. <table><tbody><tr><th>install</th><td><code>yarn add react-aria-components</code></td></tr><tr><th>version</th><td>1.10.1</td></tr><tr><th>usage</th><td><code><span>import</span> {CheckboxGroup} <span>from</span> <span>'react-aria-components'</span></code></td></tr></tbody></table> View ARIA pattern W3C View repository GitHub View package NPM ## Example# * * * import {CheckboxGroup, Checkbox, Label} from 'react-aria-components'; <CheckboxGroup> <Label>Favorite sports</Label> <Checkbox value="soccer"> <div className="checkbox" aria-hidden="true"> <svg viewBox="0 0 18 18"><polyline points="1 9 7 14 15 4" /></svg> </div> Soccer </Checkbox> <Checkbox value="baseball"> <div className="checkbox" aria-hidden="true"> <svg viewBox="0 0 18 18"><polyline points="1 9 7 14 15 4" /></svg> </div> Baseball </Checkbox> <Checkbox value="basketball"> <div className="checkbox" aria-hidden="true"> <svg viewBox="0 0 18 18"><polyline points="1 9 7 14 15 4" /></svg> </div> Basketball </Checkbox> </CheckboxGroup> import { Checkbox, CheckboxGroup, Label } from 'react-aria-components'; <CheckboxGroup> <Label>Favorite sports</Label> <Checkbox value="soccer"> <div className="checkbox" aria-hidden="true"> <svg viewBox="0 0 18 18"> <polyline points="1 9 7 14 15 4" /> </svg> </div> Soccer </Checkbox> <Checkbox value="baseball"> <div className="checkbox" aria-hidden="true"> <svg viewBox="0 0 18 18"> <polyline points="1 9 7 14 15 4" /> </svg> </div> Baseball </Checkbox> <Checkbox value="basketball"> <div className="checkbox" aria-hidden="true"> <svg viewBox="0 0 18 18"> <polyline points="1 9 7 14 15 4" /> </svg> </div> Basketball </Checkbox> </CheckboxGroup> import { Checkbox, CheckboxGroup, Label } from 'react-aria-components'; <CheckboxGroup> <Label> Favorite sports </Label> <Checkbox value="soccer"> <div className="checkbox" aria-hidden="true" > <svg viewBox="0 0 18 18"> <polyline points="1 9 7 14 15 4" /> </svg> </div> Soccer </Checkbox> <Checkbox value="baseball"> <div className="checkbox" aria-hidden="true" > <svg viewBox="0 0 18 18"> <polyline points="1 9 7 14 15 4" /> </svg> </div> Baseball </Checkbox> <Checkbox value="basketball"> <div className="checkbox" aria-hidden="true" > <svg viewBox="0 0 18 18"> <polyline points="1 9 7 14 15 4" /> </svg> </div> Basketball </Checkbox> </CheckboxGroup> Favorite sports Soccer Baseball Basketball Show CSS @import "@react-aria/example-theme"; .react-aria-CheckboxGroup { display: flex; flex-direction: column; gap: 0.571rem; color: var(--text-color); } @import "@react-aria/example-theme"; .react-aria-CheckboxGroup { display: flex; flex-direction: column; gap: 0.571rem; color: var(--text-color); } @import "@react-aria/example-theme"; .react-aria-CheckboxGroup { display: flex; flex-direction: column; gap: 0.571rem; color: var(--text-color); } ## Features# * * * Checkbox groups can be built in HTML with the <fieldset> and <input> elements, however these can be difficult to style. `CheckboxGroup` helps achieve accessible checkbox groups that can be styled as needed. * **Accessible** – Checkbox groups are exposed to assistive technology via ARIA, and automatically associate a nested `<Label>`. Description and error message help text slots are supported as well. * **HTML form integration** – Each individual checkbox uses a visually hidden `<input>` element under the hood, which enables HTML form integration and autofill. * **Validation** – Support for native HTML constraint validation with customizable UI, custom validation functions, and server-side validation errors. ## Anatomy# * * * A checkbox group consists of a set of checkboxes, and a label. Each checkbox includes a label and a visual selection indicator. Zero or more checkboxes within the group can be selected at a time. Users may click or touch a checkbox to select it, or use the Tab key to navigate to it and the Space key to toggle it. `CheckboxGroup` also supports optional description and error message elements, which can be used to provide more context about the field, and any validation messages. These are linked with the inputs via the `aria-describedby` attribute. import {Checkbox, CheckboxGroup, FieldError, Label, Text} from 'react-aria-components'; <CheckboxGroup> <Label /> <Checkbox /> <Text slot="description" /> <FieldError /> </CheckboxGroup> import { Checkbox, CheckboxGroup, FieldError, Label, Text } from 'react-aria-components'; <CheckboxGroup> <Label /> <Checkbox /> <Text slot="description" /> <FieldError /> </CheckboxGroup> import { Checkbox, CheckboxGroup, FieldError, Label, Text } from 'react-aria-components'; <CheckboxGroup> <Label /> <Checkbox /> <Text slot="description" /> <FieldError /> </CheckboxGroup> Individual checkboxes must have a visual label. If the checkbox group does not have a visible label, an `aria-label` or `aria-labelledby` prop must be passed instead to identify the element to assistive technology. ### Concepts# `CheckboxGroup` makes use of the following concepts: Forms Validating and submitting form data, and integrating with form libraries. ### Composed components# A `CheckboxGroup` uses the following components, which may also be used standalone or reused in other components. Label A label provides context for an element. ## Starter kits# * * * To help kick-start your project, we offer starter kits that include example implementations of all React Aria components with various styling solutions. All components are fully styled, including support for dark mode, high contrast mode, and all UI states. Each starter comes with a pre-configured Storybook that you can experiment with, or use as a starting point for your own component library. Vanilla CSS Download ZIP Preview Tailwind CSS Download ZIP Preview ## Reusable wrappers# * * * If you will use a CheckboxGroup in multiple places in your app, you can wrap all of the pieces into a reusable component. This way, the DOM structure, styling code, and other logic are defined in a single place and reused everywhere to ensure consistency. This example wraps `CheckboxGroup` and all of its children together into a single component which accepts a `label` prop, which is passed to the right place. It also shows how to use the `description` slot to render help text, and `FieldError` component to render validation errors. import type {CheckboxGroupProps, CheckboxProps, ValidationResult} from 'react-aria-components'; import {FieldError, Text} from 'react-aria-components'; interface MyCheckboxGroupProps extends Omit<CheckboxGroupProps, 'children'> { children?: React.ReactNode; label?: string; description?: string; errorMessage?: string | ((validation: ValidationResult) => string); } export function MyCheckboxGroup({ label, description, errorMessage, children, ...props }: MyCheckboxGroupProps) { return ( <CheckboxGroup {...props}> {label && <Label>{label}</Label>} {children} {description && <Text slot="description">{description}</Text>} <FieldError>{errorMessage}</FieldError> </CheckboxGroup> ); } interface MyCheckboxProps extends Omit<CheckboxProps, 'children'> { children?: React.ReactNode; } function MyCheckbox({ children, ...props }: MyCheckboxProps) { return ( <Checkbox {...props}> <div className="checkbox" aria-hidden="true"> <svg viewBox="0 0 18 18"> <polyline points="1 9 7 14 15 4" /> </svg> </div> {children} </Checkbox> ); } <MyCheckboxGroup label="Favorite sports"> <MyCheckbox value="soccer">Soccer</MyCheckbox> <MyCheckbox value="baseball">Baseball</MyCheckbox> <MyCheckbox value="basketball">Basketball</MyCheckbox> </MyCheckboxGroup> import type { CheckboxGroupProps, CheckboxProps, ValidationResult } from 'react-aria-components'; import {FieldError, Text} from 'react-aria-components'; interface MyCheckboxGroupProps extends Omit<CheckboxGroupProps, 'children'> { children?: React.ReactNode; label?: string; description?: string; errorMessage?: | string | ((validation: ValidationResult) => string); } export function MyCheckboxGroup({ label, description, errorMessage, children, ...props }: MyCheckboxGroupProps) { return ( <CheckboxGroup {...props}> {label && <Label>{label}</Label>} {children} {description && ( <Text slot="description">{description}</Text> )} <FieldError>{errorMessage}</FieldError> </CheckboxGroup> ); } interface MyCheckboxProps extends Omit<CheckboxProps, 'children'> { children?: React.ReactNode; } function MyCheckbox( { children, ...props }: MyCheckboxProps ) { return ( <Checkbox {...props}> <div className="checkbox" aria-hidden="true"> <svg viewBox="0 0 18 18"> <polyline points="1 9 7 14 15 4" /> </svg> </div> {children} </Checkbox> ); } <MyCheckboxGroup label="Favorite sports"> <MyCheckbox value="soccer">Soccer</MyCheckbox> <MyCheckbox value="baseball">Baseball</MyCheckbox> <MyCheckbox value="basketball">Basketball</MyCheckbox> </MyCheckboxGroup> import type { CheckboxGroupProps, CheckboxProps, ValidationResult } from 'react-aria-components'; import { FieldError, Text } from 'react-aria-components'; interface MyCheckboxGroupProps extends Omit< CheckboxGroupProps, 'children' > { children?: React.ReactNode; label?: string; description?: string; errorMessage?: | string | (( validation: ValidationResult ) => string); } export function MyCheckboxGroup( { label, description, errorMessage, children, ...props }: MyCheckboxGroupProps ) { return ( <CheckboxGroup {...props} > {label && ( <Label> {label} </Label> )} {children} {description && ( <Text slot="description"> {description} </Text> )} <FieldError> {errorMessage} </FieldError> </CheckboxGroup> ); } interface MyCheckboxProps extends Omit< CheckboxProps, 'children' > { children?: React.ReactNode; } function MyCheckbox( { children, ...props }: MyCheckboxProps ) { return ( <Checkbox {...props}> <div className="checkbox" aria-hidden="true" > <svg viewBox="0 0 18 18"> <polyline points="1 9 7 14 15 4" /> </svg> </div> {children} </Checkbox> ); } <MyCheckboxGroup label="Favorite sports"> <MyCheckbox value="soccer"> Soccer </MyCheckbox> <MyCheckbox value="baseball"> Baseball </MyCheckbox> <MyCheckbox value="basketball"> Basketball </MyCheckbox> </MyCheckboxGroup> Favorite sports Soccer Baseball Basketball ## Value# * * * ### Default value# An initial, uncontrolled value can be provided to the CheckboxGroup using the `defaultValue` prop, which accepts an array of selected items that map to the `value` prop on each Checkbox. <MyCheckboxGroup label="Favorite sports (uncontrolled)" defaultValue={['soccer', 'baseball']} > <MyCheckbox value="soccer">Soccer</MyCheckbox> <MyCheckbox value="baseball">Baseball</MyCheckbox> <MyCheckbox value="basketball">Basketball</MyCheckbox> </MyCheckboxGroup> <MyCheckboxGroup label="Favorite sports (uncontrolled)" defaultValue={['soccer', 'baseball']} > <MyCheckbox value="soccer">Soccer</MyCheckbox> <MyCheckbox value="baseball">Baseball</MyCheckbox> <MyCheckbox value="basketball">Basketball</MyCheckbox> </MyCheckboxGroup> <MyCheckboxGroup label="Favorite sports (uncontrolled)" defaultValue={[ 'soccer', 'baseball' ]} > <MyCheckbox value="soccer"> Soccer </MyCheckbox> <MyCheckbox value="baseball"> Baseball </MyCheckbox> <MyCheckbox value="basketball"> Basketball </MyCheckbox> </MyCheckboxGroup> Favorite sports (uncontrolled) Soccer Baseball Basketball ### Controlled value# A controlled value can be provided using the `value` prop, which accepts an array of selected items, which map to the `value` prop on each Checkbox. The `onChange` event is fired when the user checks or unchecks an option. It receives a new array containing the updated selected values. function Example() { let [selected, setSelected] = React.useState(['soccer', 'baseball']); return ( <MyCheckboxGroup label="Favorite sports (controlled)" value={selected} onChange={setSelected} > <MyCheckbox value="soccer">Soccer</MyCheckbox> <MyCheckbox value="baseball">Baseball</MyCheckbox> <MyCheckbox value="basketball">Basketball</MyCheckbox> </MyCheckboxGroup> ); } function Example() { let [selected, setSelected] = React.useState([ 'soccer', 'baseball' ]); return ( <MyCheckboxGroup label="Favorite sports (controlled)" value={selected} onChange={setSelected} > <MyCheckbox value="soccer">Soccer</MyCheckbox> <MyCheckbox value="baseball">Baseball</MyCheckbox> <MyCheckbox value="basketball">Basketball</MyCheckbox> </MyCheckboxGroup> ); } function Example() { let [ selected, setSelected ] = React.useState([ 'soccer', 'baseball' ]); return ( <MyCheckboxGroup label="Favorite sports (controlled)" value={selected} onChange={setSelected} > <MyCheckbox value="soccer"> Soccer </MyCheckbox> <MyCheckbox value="baseball"> Baseball </MyCheckbox> <MyCheckbox value="basketball"> Basketball </MyCheckbox> </MyCheckboxGroup> ); } Favorite sports (controlled) Soccer Baseball Basketball ### HTML forms# CheckboxGroup supports the `name` prop, paired with the Checkbox `value` prop, for integration with HTML forms. <MyCheckboxGroup label="Favorite sports" name="sports"> <MyCheckbox value="soccer">Soccer</MyCheckbox> <MyCheckbox value="baseball">Baseball</MyCheckbox> <MyCheckbox value="basketball">Basketball</MyCheckbox> </MyCheckboxGroup> <MyCheckboxGroup label="Favorite sports" name="sports"> <MyCheckbox value="soccer">Soccer</MyCheckbox> <MyCheckbox value="baseball">Baseball</MyCheckbox> <MyCheckbox value="basketball">Basketball</MyCheckbox> </MyCheckboxGroup> <MyCheckboxGroup label="Favorite sports" name="sports" > <MyCheckbox value="soccer"> Soccer </MyCheckbox> <MyCheckbox value="baseball"> Baseball </MyCheckbox> <MyCheckbox value="basketball"> Basketball </MyCheckbox> </MyCheckboxGroup> Favorite sports Soccer Baseball Basketball ## Validation# * * * CheckboxGroup supports the `isRequired` prop to ensure the user selects at least one item, as well as custom client and server-side validation. Individual checkboxes also support validation, and errors from all checkboxes are aggregated at the group level. CheckboxGroup can also be integrated with other form libraries. See the Forms guide to learn more. ### Group validation# The `isRequired` prop at the `CheckboxGroup` level requires that at least one item is selected. To display validation errors, add a `<FieldError>` element as a child of the CheckboxGroup. This allows you to render error messages from all of the above sources with consistent custom styles. import {Form, FieldError, Button} from 'react-aria-components'; <Form> <CheckboxGroup name="condiments" isRequired> <Label>Sandwich condiments</Label> <MyCheckbox value="lettuce">Lettuce</MyCheckbox> <MyCheckbox value="tomato">Tomato</MyCheckbox> <MyCheckbox value="onion">Onion</MyCheckbox> <MyCheckbox value="sprouts">Sprouts</MyCheckbox> <FieldError /> </CheckboxGroup> <Button type="submit">Submit</Button> </Form> import { Button, FieldError, Form } from 'react-aria-components'; <Form> <CheckboxGroup name="condiments" isRequired> <Label>Sandwich condiments</Label> <MyCheckbox value="lettuce">Lettuce</MyCheckbox> <MyCheckbox value="tomato">Tomato</MyCheckbox> <MyCheckbox value="onion">Onion</MyCheckbox> <MyCheckbox value="sprouts">Sprouts</MyCheckbox> <FieldError /> </CheckboxGroup> <Button type="submit">Submit</Button> </Form> import { Button, FieldError, Form } from 'react-aria-components'; <Form> <CheckboxGroup name="condiments" isRequired > <Label> Sandwich condiments </Label> <MyCheckbox value="lettuce"> Lettuce </MyCheckbox> <MyCheckbox value="tomato"> Tomato </MyCheckbox> <MyCheckbox value="onion"> Onion </MyCheckbox> <MyCheckbox value="sprouts"> Sprouts </MyCheckbox> <FieldError /> </CheckboxGroup> <Button type="submit"> Submit </Button> </Form> Sandwich condiments Lettuce Tomato Onion Sprouts Submit Show CSS .react-aria-CheckboxGroup { .react-aria-FieldError { font-size: 12px; color: var(--invalid-color); } } .react-aria-CheckboxGroup { .react-aria-FieldError { font-size: 12px; color: var(--invalid-color); } } .react-aria-CheckboxGroup { .react-aria-FieldError { font-size: 12px; color: var(--invalid-color); } } By default, `FieldError` displays default validation messages provided by the browser. See Customizing error messages in the Forms guide to learn how to provide your own custom errors. ### Individual Checkbox validation# To require that specific checkboxes are checked, set the `isRequired` prop at the `Checkbox` level instead of the `CheckboxGroup`. The following example shows how to require that all items are selected. <Form> <CheckboxGroup> <Label>Agree to the following</Label> <MyCheckbox value="terms" isRequired>Terms and conditions</MyCheckbox> <MyCheckbox value="privacy" isRequired>Privacy policy</MyCheckbox> <MyCheckbox value="cookies" isRequired>Cookie policy</MyCheckbox> <FieldError /> </CheckboxGroup> <Button type="submit">Submit</Button> </Form> <Form> <CheckboxGroup> <Label>Agree to the following</Label> <MyCheckbox value="terms" isRequired> Terms and conditions </MyCheckbox> <MyCheckbox value="privacy" isRequired> Privacy policy </MyCheckbox> <MyCheckbox value="cookies" isRequired> Cookie policy </MyCheckbox> <FieldError /> </CheckboxGroup> <Button type="submit">Submit</Button> </Form> <Form> <CheckboxGroup> <Label> Agree to the following </Label> <MyCheckbox value="terms" isRequired > Terms and conditions </MyCheckbox> <MyCheckbox value="privacy" isRequired > Privacy policy </MyCheckbox> <MyCheckbox value="cookies" isRequired > Cookie policy </MyCheckbox> <FieldError /> </CheckboxGroup> <Button type="submit"> Submit </Button> </Form> Agree to the following Terms and conditions Privacy policy Cookie policy Submit ### Description# The `description` slot can be used to associate additional help text with a checkbox group. <CheckboxGroup> <Label>Pets</Label> <MyCheckbox value="dogs">Dogs</MyCheckbox> <MyCheckbox value="cats">Cats</MyCheckbox> <MyCheckbox value="dragons">Dragons</MyCheckbox> <Text slot="description">Select your pets.</Text> </CheckboxGroup> <CheckboxGroup> <Label>Pets</Label> <MyCheckbox value="dogs">Dogs</MyCheckbox> <MyCheckbox value="cats">Cats</MyCheckbox> <MyCheckbox value="dragons">Dragons</MyCheckbox> <Text slot="description">Select your pets.</Text> </CheckboxGroup> <CheckboxGroup> <Label>Pets</Label> <MyCheckbox value="dogs"> Dogs </MyCheckbox> <MyCheckbox value="cats"> Cats </MyCheckbox> <MyCheckbox value="dragons"> Dragons </MyCheckbox> <Text slot="description"> Select your pets. </Text> </CheckboxGroup> Pets Dogs Cats DragonsSelect your pets. Show CSS .react-aria-CheckboxGroup { [slot=description] { font-size: 12px; } } .react-aria-CheckboxGroup { [slot=description] { font-size: 12px; } } .react-aria-CheckboxGroup { [slot=description] { font-size: 12px; } } ## Disabled# * * * The entire CheckboxGroup can be disabled with the `isDisabled` prop. <MyCheckboxGroup label="Favorite sports" isDisabled> <MyCheckbox value="soccer">Soccer</MyCheckbox> <MyCheckbox value="baseball">Baseball</MyCheckbox> <MyCheckbox value="basketball">Basketball</MyCheckbox> </MyCheckboxGroup> <MyCheckboxGroup label="Favorite sports" isDisabled> <MyCheckbox value="soccer">Soccer</MyCheckbox> <MyCheckbox value="baseball">Baseball</MyCheckbox> <MyCheckbox value="basketball">Basketball</MyCheckbox> </MyCheckboxGroup> <MyCheckboxGroup label="Favorite sports" isDisabled > <MyCheckbox value="soccer"> Soccer </MyCheckbox> <MyCheckbox value="baseball"> Baseball </MyCheckbox> <MyCheckbox value="basketball"> Basketball </MyCheckbox> </MyCheckboxGroup> Favorite sports Soccer Baseball Basketball To disable an individual checkbox, pass `isDisabled` to the `Checkbox` instead. <MyCheckboxGroup label="Favorite sports"> <MyCheckbox value="soccer">Soccer</MyCheckbox> <MyCheckbox value="baseball" isDisabled>Baseball</MyCheckbox> <MyCheckbox value="basketball">Basketball</MyCheckbox> </MyCheckboxGroup> <MyCheckboxGroup label="Favorite sports"> <MyCheckbox value="soccer">Soccer</MyCheckbox> <MyCheckbox value="baseball" isDisabled> Baseball </MyCheckbox> <MyCheckbox value="basketball">Basketball</MyCheckbox> </MyCheckboxGroup> <MyCheckboxGroup label="Favorite sports"> <MyCheckbox value="soccer"> Soccer </MyCheckbox> <MyCheckbox value="baseball" isDisabled > Baseball </MyCheckbox> <MyCheckbox value="basketball"> Basketball </MyCheckbox> </MyCheckboxGroup> Favorite sports Soccer Baseball Basketball ### Read only# The `isReadOnly` prop makes the selection immutable. Unlike `isDisabled`, the CheckboxGroup remains focusable. See the MDN docs for more information. <MyCheckboxGroup label="Favorite sports" defaultValue={['baseball']} isReadOnly > <MyCheckbox value="soccer">Soccer</MyCheckbox> <MyCheckbox value="baseball">Baseball</MyCheckbox> <MyCheckbox value="basketball">Basketball</MyCheckbox> </MyCheckboxGroup> <MyCheckboxGroup label="Favorite sports" defaultValue={['baseball']} isReadOnly > <MyCheckbox value="soccer">Soccer</MyCheckbox> <MyCheckbox value="baseball">Baseball</MyCheckbox> <MyCheckbox value="basketball">Basketball</MyCheckbox> </MyCheckboxGroup> <MyCheckboxGroup label="Favorite sports" defaultValue={[ 'baseball' ]} isReadOnly > <MyCheckbox value="soccer"> Soccer </MyCheckbox> <MyCheckbox value="baseball"> Baseball </MyCheckbox> <MyCheckbox value="basketball"> Basketball </MyCheckbox> </MyCheckboxGroup> Favorite sports Soccer Baseball Basketball ## Props# * * * ### CheckboxGroup# | Name | Type | Default | Description | | --- | --- | --- | --- | | `value` | `string[]` | — | The current value (controlled). | | `defaultValue` | `string[]` | — | The default value (uncontrolled). | | `isDisabled` | `boolean` | — | Whether the input is disabled. | | `isReadOnly` | `boolean` | — | Whether the input can be selected but not changed by the user. | | `name` | `string` | — | The name of the input element, used when submitting an HTML form. See MDN. | | `isRequired` | `boolean` | — | Whether user input is required on the input before form submission. | | `isInvalid` | `boolean` | — | Whether the input value is invalid. | | `validate` | `( (value: string[] )) => ValidationError | true | null | undefined` | — | A function that returns an error message if a given value is invalid. Validation errors are displayed to the user when the form is submitted if `validationBehavior="native"`. For realtime validation, use the `isInvalid` prop instead. | | `validationBehavior` | `'native' | 'aria'` | `'native'` | Whether to use native HTML form validation to prevent form submission when the value is missing or invalid, or mark the field as required or invalid via ARIA. | | `children` | `ReactNode | ( (values: CheckboxGroupRenderProps & & { defaultChildren: ReactNode | | undefined } )) => ReactNode` | — | The children of the component. A function may be provided to alter the children based on component state. | | `className` | `string | ( (values: CheckboxGroupRenderProps & & { defaultClassName: string | | undefined } )) => string` | — | The CSS className for the element. A function may be provided to compute the class based on component state. | | `style` | `CSSProperties | ( (values: CheckboxGroupRenderProps & & { defaultStyle: CSSProperties } )) => CSSProperties | undefined` | — | The inline style for the element. A function may be provided to compute the style based on component state. | Events | Name | Type | Description | | --- | --- | --- | | `onChange` | `( (value: T )) => void` | Handler that is called when the value changes. | | `onFocus` | `( (e: FocusEvent<Target> )) => void` | Handler that is called when the element receives focus. | | `onBlur` | `( (e: FocusEvent<Target> )) => void` | Handler that is called when the element loses focus. | | `onFocusChange` | `( (isFocused: boolean )) => void` | Handler that is called when the element's focus status changes. | Layout | Name | Type | Description | | --- | --- | --- | | `slot` | `string | null` | A slot name for the component. Slots allow the component to receive props from a parent component. An explicit `null` value indicates that the local props completely override all props received from a parent. | Accessibility | Name | Type | Description | | --- | --- | --- | | `id` | `string` | The element's unique identifier. See MDN. | | `aria-label` | `string` | Defines a string value that labels the current element. | | `aria-labelledby` | `string` | Identifies the element (or elements) that labels the current element. | | `aria-describedby` | `string` | Identifies the element (or elements) that describes the object. | | `aria-details` | `string` | Identifies the element (or elements) that provide a detailed, extended description for the object. | | `aria-errormessage` | `string` | Identifies the element that provides an error message for the object. | ### Checkbox# Within a `<CheckboxGroup>`, most `<Checkbox>` props are set automatically. The `value` prop is required to identify the checkbox within the group. Show props | Name | Type | Default | Description | | --- | --- | --- | --- | | `inputRef` | ` RefObject <HTMLInputElement | null>` | — | A ref for the HTML input element. | | `isIndeterminate` | `boolean` | — | Indeterminism is presentational only. The indeterminate visual representation remains regardless of user interaction. | | `value` | `string` | — | The value of the input element, used when submitting an HTML form. See MDN. | | `defaultSelected` | `boolean` | — | Whether the element should be selected (uncontrolled). | | `isSelected` | `boolean` | — | Whether the element should be selected (controlled). | | `isDisabled` | `boolean` | — | Whether the input is disabled. | | `isReadOnly` | `boolean` | — | Whether the input can be selected but not changed by the user. | | `isRequired` | `boolean` | — | Whether user input is required on the input before form submission. | | `isInvalid` | `boolean` | — | Whether the input value is invalid. | | `validate` | `( (value: boolean )) => ValidationError | true | null | undefined` | — | A function that returns an error message if a given value is invalid. Validation errors are displayed to the user when the form is submitted if `validationBehavior="native"`. For realtime validation, use the `isInvalid` prop instead. | | `autoFocus` | `boolean` | — | Whether the element should receive focus on render. | | `name` | `string` | — | The name of the input element, used when submitting an HTML form. See MDN. | | `validationBehavior` | `'native' | 'aria'` | `'native'` | Whether to use native HTML form validation to prevent form submission when the value is missing or invalid, or mark the field as required or invalid via ARIA. | | `children` | `ReactNode | ( (values: CheckboxRenderProps & & { defaultChildren: ReactNode | | undefined } )) => ReactNode` | — | The children of the component. A function may be provided to alter the children based on component state. | | `className` | `string | ( (values: CheckboxRenderProps & & { defaultClassName: string | | undefined } )) => string` | — | The CSS className for the element. A function may be provided to compute the class based on component state. | | `style` | `CSSProperties | ( (values: CheckboxRenderProps & & { defaultStyle: CSSProperties } )) => CSSProperties | undefined` | — | The inline style for the element. A function may be provided to compute the style based on component state. | Events | Name | Type | Description | | --- | --- | --- | | `onChange` | `( (isSelected: boolean )) => void` | Handler that is called when the element's selection state changes. | | `onFocus` | `( (e: FocusEvent<Target> )) => void` | Handler that is called when the element receives focus. | | `onBlur` | `( (e: FocusEvent<Target> )) => void` | Handler that is called when the element loses focus. | | `onFocusChange` | `( (isFocused: boolean )) => void` | Handler that is called when the element's focus status changes. | | `onKeyDown` | `( (e: KeyboardEvent )) => void` | Handler that is called when a key is pressed. | | `onKeyUp` | `( (e: KeyboardEvent )) => void` | Handler that is called when a key is released. | | `onHoverStart` | `( (e: HoverEvent )) => void` | Handler that is called when a hover interaction starts. | | `onHoverEnd` | `( (e: HoverEvent )) => void` | Handler that is called when a hover interaction ends. | | `onHoverChange` | `( (isHovering: boolean )) => void` | Handler that is called when the hover state changes. | Layout | Name | Type | Description | | --- | --- | --- | | `slot` | `string | null` | A slot name for the component. Slots allow the component to receive props from a parent component. An explicit `null` value indicates that the local props completely override all props received from a parent. | Accessibility | Name | Type | Description | | --- | --- | --- | | `id` | `string` | The element's unique identifier. See MDN. | | `excludeFromTabOrder` | `boolean` | Whether to exclude the element from the sequential tab order. If true, the element will not be focusable via the keyboard by tabbing. This should be avoided except in rare scenarios where an alternative means of accessing the element or its functionality via the keyboard is available. | | `aria-controls` | `string` | Identifies the element (or elements) whose contents or presence are controlled by the current element. | | `aria-label` | `string` | Defines a string value that labels the current element. | | `aria-labelledby` | `string` | Identifies the element (or elements) that labels the current element. | | `aria-describedby` | `string` | Identifies the element (or elements) that describes the object. | | `aria-details` | `string` | Identifies the element (or elements) that provide a detailed, extended description for the object. | | `aria-errormessage` | `string` | Identifies the element that provides an error message for the object. | ### Text# `<Text>` accepts all HTML attributes. ### FieldError# A `<FieldError>` displays validation errors. Show props | Name | Type | Description | | --- | --- | --- | | `children` | `ReactNode | ( (values: FieldErrorRenderProps & & { defaultChildren: ReactNode | | undefined } )) => ReactNode` | The children of the component. A function may be provided to alter the children based on component state. | | `className` | `string | ( (values: FieldErrorRenderProps & & { defaultClassName: string | | undefined } )) => string` | The CSS className for the element. A function may be provided to compute the class based on component state. | | `style` | `CSSProperties | ( (values: FieldErrorRenderProps & & { defaultStyle: CSSProperties } )) => CSSProperties | undefined` | The inline style for the element. A function may be provided to compute the style based on component state. | Accessibility | Name | Type | Description | | --- | --- | --- | | `id` | `string` | The element's unique identifier. See MDN. | ## Styling# * * * React Aria components can be styled in many ways, including using CSS classes, inline styles, utility classes (e.g. Tailwind), CSS-in-JS (e.g. Styled Components), etc. By default, all components include a builtin `className` attribute which can be targeted using CSS selectors. These follow the `react-aria-ComponentName` naming convention. .react-aria-CheckboxGroup { /* ... */ } .react-aria-CheckboxGroup { /* ... */ } .react-aria-CheckboxGroup { /* ... */ } A custom `className` can also be specified on any component. This overrides the default `className` provided by React Aria with your own. <Checkbox className="my-checkbox"> {/* ... */} </Checkbox> <Checkbox className="my-checkbox"> {/* ... */} </Checkbox> <Checkbox className="my-checkbox"> {/* ... */} </Checkbox> In addition, some components support multiple UI states (e.g. focused, placeholder, readonly, etc.). React Aria components expose states using data attributes, which you can target in CSS selectors. For example: .react-aria-Checkbox[data-selected] { /* ... */ } .react-aria-Checkbox[data-selected] { /* ... */ } .react-aria-Checkbox[data-selected] { /* ... */ } The `className` and `style` props also accept functions which receive states for styling. This lets you dynamically determine the classes or styles to apply, which is useful when using utility CSS libraries like Tailwind. <Checkbox className={({ isPressed }) => isPressed ? 'bg-gray-700' : 'bg-gray-600'} /> <Checkbox className={({ isPressed }) => isPressed ? 'bg-gray-700' : 'bg-gray-600'} /> <Checkbox className={( { isPressed } ) => isPressed ? 'bg-gray-700' : 'bg-gray-600'} /> Render props may also be used as children to alter what elements are rendered based on the current state. For example, you could render an extra element when the checkbox is selected. <Checkbox> {({isSelected}) => ( <> {isSelected && <CheckIcon />} Subscribe </> )} </Checkbox> <Checkbox> {({isSelected}) => ( <> {isSelected && <CheckIcon />} Subscribe </> )} </Checkbox> <Checkbox> {( { isSelected } ) => ( <> {isSelected && ( <CheckIcon /> )} Subscribe </> )} </Checkbox> The states and selectors for each component used in a `CheckboxGroup` are documented below. ### CheckboxGroup# A `CheckboxGroup` can be targeted with the `.react-aria-CheckboxGroup` CSS selector, or by overriding with a custom `className`. It supports the following states and render props: | Name | CSS Selector | Description | | --- | --- | --- | | `isDisabled` | `[data-disabled]` | Whether the checkbox group is disabled. | | `isReadOnly` | `[data-readonly]` | Whether the checkbox group is read only. | | `isRequired` | `[data-required]` | Whether the checkbox group is required. | | `isInvalid` | `[data-invalid]` | Whether the checkbox group is invalid. | | `state` | `—` | State of the checkbox group. | ### Checkbox# A `Checkbox` can be targeted with the `.react-aria-Checkbox` CSS selector, or by overriding with a custom `className`. It supports the following states: | Name | CSS Selector | Description | | --- | --- | --- | | `isSelected` | `[data-selected]` | Whether the checkbox is selected. | | `isIndeterminate` | `[data-indeterminate]` | Whether the checkbox is indeterminate. | | `isHovered` | `[data-hovered]` | Whether the checkbox is currently hovered with a mouse. | | `isPressed` | `[data-pressed]` | Whether the checkbox is currently in a pressed state. | | `isFocused` | `[data-focused]` | Whether the checkbox is focused, either via a mouse or keyboard. | | `isFocusVisible` | `[data-focus-visible]` | Whether the checkbox is keyboard focused. | | `isDisabled` | `[data-disabled]` | Whether the checkbox is disabled. | | `isReadOnly` | `[data-readonly]` | Whether the checkbox is read only. | | `isInvalid` | `[data-invalid]` | Whether the checkbox invalid. | | `isRequired` | `[data-required]` | Whether the checkbox is required. | ### Text# The help text elements within a `CheckboxGroup` can be targeted with the `[slot=description]` and `[slot=errorMessage]` CSS selectors, or by adding a custom `className`. ### FieldError# A `FieldError` can be targeted with the `.react-aria-FieldError` CSS selector, or by overriding with a custom `className`. It supports the following render props: | Name | Description | | --- | --- | | `isInvalid` | Whether the input value is invalid. | | `validationErrors` | The current error messages for the input if it is invalid, otherwise an empty array. | | `validationDetails` | The native validation details for the input. | ## Advanced customization# * * * ### Contexts# All React Aria Components export a corresponding context that can be used to send props to them from a parent element. This enables you to build your own compositional APIs similar to those found in React Aria Components itself. You can send any prop or ref via context that you could pass to the corresponding component. The local props and ref on the component are merged with the ones passed via context, with the local props taking precedence (following the rules documented in mergeProps). The components in a CheckboxGroup support the following contexts: | Component | Context | Props | Ref | | --- | --- | --- | --- | | `CheckboxGroup` | `CheckboxGroupContext` | ` CheckboxGroupProps ` | `HTMLDivElement` | | `Checkbox` | `CheckboxContext` | ` CheckboxProps ` | `HTMLLabelElement` | This example shows a `CheckboxDescription` component that accepts a checkbox in its children and renders a description element below it. It uses the useId hook to generate a unique id for the description, and associates it with the checkbox via the `aria-describedby` attribute passed to the `CheckboxContext` provider. import {CheckboxContext} from 'react-aria-components'; import {useId} from 'react-aria'; interface CheckboxDescriptionProps { children?: React.ReactNode; description?: string; } function CheckboxDescription( { children, description }: CheckboxDescriptionProps ) { let descriptionId = useId(); return ( <div> <CheckboxContext.Provider value={{ 'aria-describedby': descriptionId }}> {children} </CheckboxContext.Provider> <small id={descriptionId}>{description}</small> </div> ); } <MyCheckboxGroup label="Email settings" defaultValue={['newsletter', 'deals', 'notifications']} > <CheckboxDescription description="Receive our newsletter once per week."> <MyCheckbox value="newsletter">Newsletter</MyCheckbox> </CheckboxDescription> <CheckboxDescription description="The best deals and sales for members."> <MyCheckbox value="deals">Deals</MyCheckbox> </CheckboxDescription> <CheckboxDescription description="Notifications about your orders."> <MyCheckbox value="notifications">Notifications</MyCheckbox> </CheckboxDescription> </MyCheckboxGroup> import {CheckboxContext} from 'react-aria-components'; import {useId} from 'react-aria'; interface CheckboxDescriptionProps { children?: React.ReactNode; description?: string; } function CheckboxDescription( { children, description }: CheckboxDescriptionProps ) { let descriptionId = useId(); return ( <div> <CheckboxContext.Provider value={{ 'aria-describedby': descriptionId }} > {children} </CheckboxContext.Provider> <small id={descriptionId}>{description}</small> </div> ); } <MyCheckboxGroup label="Email settings" defaultValue={['newsletter', 'deals', 'notifications']} > <CheckboxDescription description="Receive our newsletter once per week."> <MyCheckbox value="newsletter">Newsletter</MyCheckbox> </CheckboxDescription> <CheckboxDescription description="The best deals and sales for members."> <MyCheckbox value="deals">Deals</MyCheckbox> </CheckboxDescription> <CheckboxDescription description="Notifications about your orders."> <MyCheckbox value="notifications"> Notifications </MyCheckbox> </CheckboxDescription> </MyCheckboxGroup> import {CheckboxContext} from 'react-aria-components'; import {useId} from 'react-aria'; interface CheckboxDescriptionProps { children?: React.ReactNode; description?: string; } function CheckboxDescription( { children, description }: CheckboxDescriptionProps ) { let descriptionId = useId(); return ( <div> <CheckboxContext.Provider value={{ 'aria-describedby': descriptionId }} > {children} </CheckboxContext.Provider> <small id={descriptionId} > {description} </small> </div> ); } <MyCheckboxGroup label="Email settings" defaultValue={[ 'newsletter', 'deals', 'notifications' ]} > <CheckboxDescription description="Receive our newsletter once per week."> <MyCheckbox value="newsletter"> Newsletter </MyCheckbox> </CheckboxDescription> <CheckboxDescription description="The best deals and sales for members."> <MyCheckbox value="deals"> Deals </MyCheckbox> </CheckboxDescription> <CheckboxDescription description="Notifications about your orders."> <MyCheckbox value="notifications"> Notifications </MyCheckbox> </CheckboxDescription> </MyCheckboxGroup> Email settings NewsletterReceive our newsletter once per week. DealsThe best deals and sales for members. NotificationsNotifications about your orders. ### Custom children# CheckboxGroup passes props to its child components, such as the label, via their associated contexts. These contexts are exported so you can also consume them in your own custom components. This enables you to reuse existing components from your app or component library together with React Aria Components. | Component | Context | Props | Ref | | --- | --- | --- | --- | | `Label` | `LabelContext` | ` LabelProps ` | `HTMLLabelElement` | | `Text` | `TextContext` | ` TextProps ` | `HTMLElement` | This example consumes from `LabelContext` in an existing styled label component to make it compatible with React Aria Components. The `useContextProps` hook merges the local props and ref with the ones provided via context by CheckboxGroup. import type {LabelProps} from 'react-aria-components'; import {LabelContext, useContextProps} from 'react-aria-components'; const MyCustomLabel = React.forwardRef( (props: LabelProps, ref: React.ForwardedRef<HTMLLabelElement>) => { // Merge the local props and ref with the ones provided via context. [props, ref] = useContextProps(props, ref, LabelContext); // ... your existing Label component return <label {...props} ref={ref} />; } ); import type {LabelProps} from 'react-aria-components'; import { LabelContext, useContextProps } from 'react-aria-components'; const MyCustomLabel = React.forwardRef( ( props: LabelProps, ref: React.ForwardedRef<HTMLLabelElement> ) => { // Merge the local props and ref with the ones provided via context. [props, ref] = useContextProps( props, ref, LabelContext ); // ... your existing Label component return <label {...props} ref={ref} />; } ); import type {LabelProps} from 'react-aria-components'; import { LabelContext, useContextProps } from 'react-aria-components'; const MyCustomLabel = React.forwardRef( ( props: LabelProps, ref: React.ForwardedRef< HTMLLabelElement > ) => { // Merge the local props and ref with the ones provided via context. [props, ref] = useContextProps( props, ref, LabelContext ); // ... your existing Label component return ( <label {...props} ref={ref} /> ); } ); Now you can use `MyCustomLabel` within a `CheckboxGroup`, in place of the builtin React Aria Components `Label`. <CheckboxGroup> <MyCustomLabel>Favorite sports</MyCustomLabel> <MyCheckbox value="soccer">Soccer</MyCheckbox> <MyCheckbox value="baseball">Baseball</MyCheckbox> <MyCheckbox value="basketball">Basketball</MyCheckbox> </CheckboxGroup> <CheckboxGroup> <MyCustomLabel>Favorite sports</MyCustomLabel> <MyCheckbox value="soccer">Soccer</MyCheckbox> <MyCheckbox value="baseball">Baseball</MyCheckbox> <MyCheckbox value="basketball">Basketball</MyCheckbox> </CheckboxGroup> <CheckboxGroup> <MyCustomLabel> Favorite sports </MyCustomLabel> <MyCheckbox value="soccer"> Soccer </MyCheckbox> <MyCheckbox value="baseball"> Baseball </MyCheckbox> <MyCheckbox value="basketball"> Basketball </MyCheckbox> </CheckboxGroup> ### State# CheckboxGroup provides a `CheckboxGroupState` object to its children via `CheckboxGroupStateContext`. This can be used to access and manipulate the checkbox group's state. This example shows a `SelectionCount` component that can be placed within a `CheckboxGroup` to display the number of selected items. import {CheckboxGroupStateContext} from 'react-aria-components'; function SelectionCount() { let state = React.useContext(CheckboxGroupStateContext)!; return <small>{state.value.length} items selected.</small>; } <MyCheckboxGroup label="Sandwich condiments"> <MyCheckbox value="lettuce">Lettuce</MyCheckbox> <MyCheckbox value="tomato">Tomato</MyCheckbox> <MyCheckbox value="onion">Onion</MyCheckbox> <MyCheckbox value="sprouts">Sprouts</MyCheckbox> <SelectionCount /> </MyCheckboxGroup> import {CheckboxGroupStateContext} from 'react-aria-components'; function SelectionCount() { let state = React.useContext(CheckboxGroupStateContext)!; return <small>{state.value.length} items selected. </small>; } <MyCheckboxGroup label="Sandwich condiments"> <MyCheckbox value="lettuce">Lettuce</MyCheckbox> <MyCheckbox value="tomato">Tomato</MyCheckbox> <MyCheckbox value="onion">Onion</MyCheckbox> <MyCheckbox value="sprouts">Sprouts</MyCheckbox> <SelectionCount /> </MyCheckboxGroup> import {CheckboxGroupStateContext} from 'react-aria-components'; function SelectionCount() { let state = React .useContext( CheckboxGroupStateContext )!; return ( <small> {state.value .length}{' '} items selected. </small> ); } <MyCheckboxGroup label="Sandwich condiments"> <MyCheckbox value="lettuce"> Lettuce </MyCheckbox> <MyCheckbox value="tomato"> Tomato </MyCheckbox> <MyCheckbox value="onion"> Onion </MyCheckbox> <MyCheckbox value="sprouts"> Sprouts </MyCheckbox> <SelectionCount /> </MyCheckboxGroup> Sandwich condiments Lettuce Tomato Onion Sprouts0 items selected. ### Hooks# If you need to customize things further, such as accessing internal state or customizing DOM structure, you can drop down to the lower level Hook-based API. See useCheckboxGroup for more details. --- ## Page: https://react-spectrum.adobe.com/react-aria/Form.html # Form A form is a group of inputs that allows users to submit data to a server, with support for providing field validation errors. <table><tbody><tr><th>install</th><td><code>yarn add react-aria-components</code></td></tr><tr><th>version</th><td>1.10.1</td></tr><tr><th>usage</th><td><code><span>import</span> {Form} <span>from</span> <span>'react-aria-components'</span></code></td></tr></tbody></table> View ARIA pattern W3C View repository GitHub View package NPM ## Example# * * * import {Button, FieldError, Form, Input, Label, TextField} from 'react-aria-components'; <Form> <TextField name="email" type="email" isRequired> <Label>Email</Label> <Input /> <FieldError /> </TextField> <Button type="submit">Submit</Button> </Form> import { Button, FieldError, Form, Input, Label, TextField } from 'react-aria-components'; <Form> <TextField name="email" type="email" isRequired> <Label>Email</Label> <Input /> <FieldError /> </TextField> <Button type="submit">Submit</Button> </Form> import { Button, FieldError, Form, Input, Label, TextField } from 'react-aria-components'; <Form> <TextField name="email" type="email" isRequired > <Label> Email </Label> <Input /> <FieldError /> </TextField> <Button type="submit"> Submit </Button> </Form> Email Submit Show CSS .react-aria-Form { display: flex; flex-direction: column; align-items: start; gap: 8px; } .react-aria-Form { display: flex; flex-direction: column; align-items: start; gap: 8px; } .react-aria-Form { display: flex; flex-direction: column; align-items: start; gap: 8px; } ## Features# * * * The HTML <form> element can be used to build forms. React Aria's `Form` component extends HTML forms with support for providing server-side validation errors to the fields within it. * **Accessible** – Uses a native `<form>` element, with support for ARIA labelling to create a form landmark. * **Validation** – Support for native HTML constraint validation with customizable UI, custom validation functions, realtime validation, and server-side validation errors. See the Forms guide to learn more about React Aria's form components, including submitting data, and form validation techniques. ## Anatomy# * * * A form consists of a container element that includes a group of input elements, typically with a button the user can press to submit data to a server. Forms may also include validation error messages, and a button to reset the form data to its initial state. If a form has an `aria-label` or `aria-labelledby` attribute, it is exposed to assistive technology as a form landmark, allowing users to quickly navigate to it. import {Form, Button} from 'react-aria-components'; <Form> {/* ... */} <Button type="submit" /> <Button type="reset" /> </Form> import {Form, Button} from 'react-aria-components'; <Form> {/* ... */} <Button type="submit" /> <Button type="reset" /> </Form> import { Button, Form } from 'react-aria-components'; <Form> {/* ... */} <Button type="submit" /> <Button type="reset" /> </Form> ## Starter kits# * * * To help kick-start your project, we offer starter kits that include example implementations of all React Aria components with various styling solutions. All components are fully styled, including support for dark mode, high contrast mode, and all UI states. Each starter comes with a pre-configured Storybook that you can experiment with, or use as a starting point for your own component library. Vanilla CSS Download ZIP Preview Tailwind CSS Download ZIP Preview ## Events# * * * The `onSubmit` event will be triggered when a user submits the form with the Enter key or by pressing a submit button. The `onReset` event will be triggered when a user presses a reset button. function Example() { let [action, setAction] = React.useState(null); return ( <Form onSubmit={e => { e.preventDefault(); let data = Object.fromEntries(new FormData(e.currentTarget)); setAction(`submit ${JSON.stringify(data)}`); }} onReset={() => setAction('reset')} > <TextField name="username" isRequired> <Label>Username</Label> <Input /> <FieldError /> </TextField> <TextField name="password" type="password" isRequired> <Label>Password</Label> <Input /> <FieldError /> </TextField> <div style={{display: 'flex', gap: 8}}> <Button type="submit">Submit</Button> <Button type="reset">Reset</Button> </div> {action && <div>Action: <code>{action}</code></div>} </Form> ); } function Example() { let [action, setAction] = React.useState(null); return ( <Form onSubmit={(e) => { e.preventDefault(); let data = Object.fromEntries( new FormData(e.currentTarget) ); setAction(`submit ${JSON.stringify(data)}`); }} onReset={() => setAction('reset')} > <TextField name="username" isRequired> <Label>Username</Label> <Input /> <FieldError /> </TextField> <TextField name="password" type="password" isRequired> <Label>Password</Label> <Input /> <FieldError /> </TextField> <div style={{ display: 'flex', gap: 8 }}> <Button type="submit">Submit</Button> <Button type="reset">Reset</Button> </div> {action && ( <div> Action: <code>{action}</code> </div> )} </Form> ); } function Example() { let [ action, setAction ] = React.useState( null ); return ( <Form onSubmit={(e) => { e.preventDefault(); let data = Object .fromEntries( new FormData( e.currentTarget ) ); setAction( `submit ${ JSON .stringify( data ) }` ); }} onReset={() => setAction( 'reset' )} > <TextField name="username" isRequired > <Label> Username </Label> <Input /> <FieldError /> </TextField> <TextField name="password" type="password" isRequired > <Label> Password </Label> <Input /> <FieldError /> </TextField> <div style={{ display: 'flex', gap: 8 }} > <Button type="submit"> Submit </Button> <Button type="reset"> Reset </Button> </div> {action && ( <div> Action:{' '} <code> {action} </code> </div> )} </Form> ); } Username Password SubmitReset ## Validation# * * * React Aria supports native HTML constraint validation with customizable UI, custom validation functions, realtime validation, and integration with server-side validation errors. The `Form` component facilitates server-side validation by providing error messages to the fields within it. To provide validation errors, the `validationErrors` prop should be set to an object that maps each field's `name` prop to a string or array of strings representing one or more errors. These are displayed to the user as soon as the `validationErrors` prop is set, and cleared after the user modifies each field's value. <Form validationErrors={{username: 'Sorry, this username is taken.'}}> <TextField name="username"> <Label>Username</Label> <Input /> <FieldError /> </TextField> </Form> <Form validationErrors={{ username: 'Sorry, this username is taken.' }} > <TextField name="username"> <Label>Username</Label> <Input /> <FieldError /> </TextField> </Form> <Form validationErrors={{ username: 'Sorry, this username is taken.' }} > <TextField name="username"> <Label> Username </Label> <Input /> <FieldError /> </TextField> </Form> UsernameSorry, this username is taken. See the Forms guide to learn more about form validation in React Aria, including client-side validation, and integration with other frameworks and libraries. ### Validation behavior# By default, native HTML form validation is used to display errors and block form submission. To instead use ARIA attributes for form validation, set the `validationBehavior` prop to "aria". This will not block form submission, and will display validation errors to the user in realtime as the value is edited. The `validationBehavior` can be set at the form level to apply to all fields, or at the field level to override the form's behavior for a specific field. <Form validationBehavior="aria"> <TextField name="username" defaultValue="admin" isRequired validate={value => value === 'admin' ? 'Nice try.' : null}> <Label>Username</Label> <Input /> <FieldError /> </TextField> <Button type="submit">Submit</Button> </Form> <Form validationBehavior="aria"> <TextField name="username" defaultValue="admin" isRequired validate={(value) => value === 'admin' ? 'Nice try.' : null} > <Label>Username</Label> <Input /> <FieldError /> </TextField> <Button type="submit">Submit</Button> </Form> <Form validationBehavior="aria"> <TextField name="username" defaultValue="admin" isRequired validate={(value) => value === 'admin' ? 'Nice try.' : null} > <Label> Username </Label> <Input /> <FieldError /> </TextField> <Button type="submit"> Submit </Button> </Form> UsernameNice try. Submit ### Focus management# By default, after a user submits a form with validation errors, the first invalid field will be focused. You can prevent this by calling `preventDefault` during the `onInvalid` event, and move focus yourself. This example shows how to move focus to an alert element at the top of a form. function Example() { let [isInvalid, setInvalid] = React.useState(false); return ( <Form onInvalid={e => { e.preventDefault(); setInvalid(true); }} onSubmit={e => { e.preventDefault(); setInvalid(false); }} onReset={() => setInvalid(false)}> {isInvalid && <div role="alert" tabIndex={-1} ref={e => e?.focus()}> <h3>Unable to submit</h3> <p>Please fix the validation errors below, and re-submit the form.</p> </div> } <TextField name="firstName" isRequired> <Label>First Name</Label> <Input /> <FieldError /> </TextField> <TextField name="lastName" isRequired> <Label>Last Name</Label> <Input /> <FieldError /> </TextField> <div style={{display: 'flex', gap: 8}}> <Button type="submit">Submit</Button> <Button type="reset">Reset</Button> </div> </Form> ); } function Example() { let [isInvalid, setInvalid] = React.useState(false); return ( <Form onInvalid={(e) => { e.preventDefault(); setInvalid(true); }} onSubmit={(e) => { e.preventDefault(); setInvalid(false); }} onReset={() => setInvalid(false)} > {isInvalid && <div role="alert" tabIndex={-1} ref={(e) => e?.focus()} > <h3>Unable to submit</h3> <p> Please fix the validation errors below, and re-submit the form. </p> </div>} <TextField name="firstName" isRequired> <Label>First Name</Label> <Input /> <FieldError /> </TextField> <TextField name="lastName" isRequired> <Label>Last Name</Label> <Input /> <FieldError /> </TextField> <div style={{ display: 'flex', gap: 8 }}> <Button type="submit">Submit</Button> <Button type="reset">Reset</Button> </div> </Form> ); } function Example() { let [ isInvalid, setInvalid ] = React.useState( false ); return ( <Form onInvalid={(e) => { e.preventDefault(); setInvalid(true); }} onSubmit={(e) => { e.preventDefault(); setInvalid( false ); }} onReset={() => setInvalid( false )} > {isInvalid && <div role="alert" tabIndex={-1} ref={(e) => e?.focus()} > <h3> Unable to submit </h3> <p> Please fix the validation errors below, and re-submit the form. </p> </div>} <TextField name="firstName" isRequired > <Label> First Name </Label> <Input /> <FieldError /> </TextField> <TextField name="lastName" isRequired > <Label> Last Name </Label> <Input /> <FieldError /> </TextField> <div style={{ display: 'flex', gap: 8 }} > <Button type="submit"> Submit </Button> <Button type="reset"> Reset </Button> </div> </Form> ); } First Name Last Name SubmitReset Show CSS .react-aria-Form [role=alert] { border: 2px solid var(--invalid-color); background: var(--overlay-background); border-radius: 6px; padding: 12px; max-width: 250px; outline: none; &:focus-visible { outline: 2px solid var(--focus-ring-color); outline-offset: 2px; } h3 { margin-top: 0; } p { margin-bottom: 0; } } .react-aria-Form [role=alert] { border: 2px solid var(--invalid-color); background: var(--overlay-background); border-radius: 6px; padding: 12px; max-width: 250px; outline: none; &:focus-visible { outline: 2px solid var(--focus-ring-color); outline-offset: 2px; } h3 { margin-top: 0; } p { margin-bottom: 0; } } .react-aria-Form [role=alert] { border: 2px solid var(--invalid-color); background: var(--overlay-background); border-radius: 6px; padding: 12px; max-width: 250px; outline: none; &:focus-visible { outline: 2px solid var(--focus-ring-color); outline-offset: 2px; } h3 { margin-top: 0; } p { margin-bottom: 0; } } ## Props# * * * | Name | Type | Default | Description | | --- | --- | --- | --- | | `validationBehavior` | `'aria' | 'native'` | `'native'` | Whether to use native HTML form validation to prevent form submission when a field value is missing or invalid, or mark fields as required or invalid via ARIA. | | `validationErrors` | ` ValidationErrors ` | — | Validation errors for the form, typically returned by a server. This should be set to an object mapping from input names to errors. | | `action` | `string | FormHTMLAttributes<HTMLFormElement>['action']` | — | Where to send the form-data when the form is submitted. See MDN. | | `encType` | `'application/x-www-form-urlencoded' | 'multipart/form-data' | 'text/plain'` | — | The enctype attribute specifies how the form-data should be encoded when submitting it to the server. See MDN. | | `method` | `'get' | 'post' | 'dialog'` | — | The HTTP method to submit the form with. See MDN. | | `target` | `'_blank' | '_self' | '_parent' | '_top'` | — | The target attribute specifies a name or a keyword that indicates where to display the response that is received after submitting the form. See MDN. | | `autoComplete` | `'off' | 'on'` | — | Indicates whether input elements can by default have their values automatically completed by the browser. See MDN. | | `autoCapitalize` | `'off' | 'none' | 'on' | 'sentences' | 'words' | 'characters'` | — | Controls whether inputted text is automatically capitalized and, if so, in what manner. See MDN. | | `children` | `ReactNode` | — | The children of the component. | | `className` | `string` | — | The CSS className for the element. | | `style` | `CSSProperties` | — | The inline style for the element. | Events | Name | Type | Description | | --- | --- | --- | | `onSubmit` | `( (event: FormEvent<HTMLFormElement> )) => void` | Triggered when a user submits the form. | | `onReset` | `( (event: FormEvent<HTMLFormElement> )) => void` | Triggered when a user resets the form. | | `onInvalid` | `( (event: FormEvent<HTMLFormElement> )) => void` | Triggered for each invalid field when a user submits the form. | Accessibility | Name | Type | Description | | --- | --- | --- | | `role` | `'search' | 'presentation'` | An ARIA role override to apply to the form element. | | `id` | `string` | The element's unique identifier. See MDN. | | `aria-label` | `string` | Defines a string value that labels the current element. | | `aria-labelledby` | `string` | Identifies the element (or elements) that labels the current element. | | `aria-describedby` | `string` | Identifies the element (or elements) that describes the object. | | `aria-details` | `string` | Identifies the element (or elements) that provide a detailed, extended description for the object. | ## Styling# * * * React Aria components can be styled in many ways, including using CSS classes, inline styles, utility classes (e.g. Tailwind), CSS-in-JS (e.g. Styled Components), etc. By default, all components include a builtin `className` attribute which can be targeted using CSS selectors. These follow the `react-aria-ComponentName` naming convention. .react-aria-Form { /* ... */ } .react-aria-Form { /* ... */ } .react-aria-Form { /* ... */ } A custom `className` can also be specified on any component. This overrides the default `className` provided by React Aria with your own. <Form className="my-form"> {/* ... */} </Form> <Form className="my-form"> {/* ... */} </Form> <Form className="my-form"> {/* ... */} </Form> ## Advanced customization# * * * ### Contexts# All React Aria Components export a corresponding context that can be used to send props to them from a parent element. This enables you to build your own compositional APIs similar to those found in React Aria Components itself. You can send any prop or ref via context that you could pass to the corresponding component. The local props and ref on the component are merged with the ones passed via context, with the local props taking precedence (following the rules documented in mergeProps). | Component | Context | Props | Ref | | --- | --- | --- | --- | | `Form` | `FormContext` | ` FormProps ` | `HTMLFormElement` | This example adds a global form submission handler for all forms rendered inside it, which could be used to centralize logic to submit data to an API. let onSubmit = e => { e.preventDefault(); // Submit form data to an API... }; <FormContext.Provider value={{onSubmit}}> <Form> {/* ... */} </Form> </FormContext.Provider> let onSubmit = e => { e.preventDefault(); // Submit form data to an API... }; <FormContext.Provider value={{onSubmit}}> <Form> {/* ... */} </Form> </FormContext.Provider> let onSubmit = (e) => { e.preventDefault(); // Submit form data to an API... }; <FormContext.Provider value={{ onSubmit }} > <Form> {/* ... */} </Form> </FormContext.Provider> `FormContext` can also be used within any component inside a form to access props from the nearest ancestor form. For example, to access the current `validationBehavior`, use the useSlottedContext hook. import {FormContext, useSlottedContext} from 'react-aria-components'; function MyFormField() { let {validationBehavior} = useSlottedContext(FormContext); // ... } <Form validationBehavior="aria"> <MyFormField /> </Form> import { FormContext, useSlottedContext } from 'react-aria-components'; function MyFormField() { let { validationBehavior } = useSlottedContext( FormContext ); // ... } <Form validationBehavior="aria"> <MyFormField /> </Form> import { FormContext, useSlottedContext } from 'react-aria-components'; function MyFormField() { let { validationBehavior } = useSlottedContext( FormContext ); // ... } <Form validationBehavior="aria"> <MyFormField /> </Form> ### Validation context# The `Form` component provides a value for `FormValidationContext`, which allows child elements to receive validation errors from the form. You can provide a value for this context directly in case you need to customize the form element, or reuse an existing form component. import {FormValidationContext} from 'react-aria-components'; <form> <FormValidationContext.Provider value={{ username: 'Sorry, this username is taken.' }} > <TextField name="username"> <Label>Username</Label> <Input /> <FieldError /> </TextField> </FormValidationContext.Provider> </form> import {FormValidationContext} from 'react-aria-components'; <form> <FormValidationContext.Provider value={{ username: 'Sorry, this username is taken.' }} > <TextField name="username"> <Label>Username</Label> <Input /> <FieldError /> </TextField> </FormValidationContext.Provider> </form> import {FormValidationContext} from 'react-aria-components'; <form> <FormValidationContext.Provider value={{ username: 'Sorry, this username is taken.' }} > <TextField name="username"> <Label> Username </Label> <Input /> <FieldError /> </TextField> </FormValidationContext.Provider> </form> UsernameSorry, this username is taken. ### Custom children# You can also consume `FormValidationContext` in your own custom form input components to receive validation errors. This example shows a native `<select>` that displays validation errors provided by `Form`. import type {SelectHTMLAttributes} from 'react'; import {useContext} from 'react'; import {useId} from 'react-aria'; function NativeSelect( props: SelectHTMLAttributes<HTMLSelectElement> & { label: string } ) { let errors = useContext(FormValidationContext); let error = errors?.[props.name]; let id = useId(); let descriptionId = useId(); return ( <div className="flex"> <label htmlFor={id}>{props.label}</label> <select {...props} id={id} aria-describedby={descriptionId} /> <small className="invalid" id={descriptionId}>{error}</small> </div> ); } <Form validationErrors={{ frequency: 'Please select a frequency.' }}> <NativeSelect label="Frequency" name="frequency"> <option value="">Select an option...</option> <option>Always</option> <option>Sometimes</option> <option>Never</option> </NativeSelect> </Form> import type {SelectHTMLAttributes} from 'react'; import {useContext} from 'react'; import {useId} from 'react-aria'; function NativeSelect( props: SelectHTMLAttributes<HTMLSelectElement> & { label: string; } ) { let errors = useContext(FormValidationContext); let error = errors?.[props.name]; let id = useId(); let descriptionId = useId(); return ( <div className="flex"> <label htmlFor={id}>{props.label}</label> <select {...props} id={id} aria-describedby={descriptionId} /> <small className="invalid" id={descriptionId}> {error} </small> </div> ); } <Form validationErrors={{ frequency: 'Please select a frequency.' }} > <NativeSelect label="Frequency" name="frequency"> <option value="">Select an option...</option> <option>Always</option> <option>Sometimes</option> <option>Never</option> </NativeSelect> </Form> import type {SelectHTMLAttributes} from 'react'; import {useContext} from 'react'; import {useId} from 'react-aria'; function NativeSelect( props: & SelectHTMLAttributes< HTMLSelectElement > & { label: string } ) { let errors = useContext( FormValidationContext ); let error = errors ?.[props.name]; let id = useId(); let descriptionId = useId(); return ( <div className="flex"> <label htmlFor={id} > {props.label} </label> <select {...props} id={id} aria-describedby={descriptionId} /> <small className="invalid" id={descriptionId} > {error} </small> </div> ); } <Form validationErrors={{ frequency: 'Please select a frequency.' }} > <NativeSelect label="Frequency" name="frequency" > <option value=""> Select an option... </option> <option> Always </option> <option> Sometimes </option> <option> Never </option> </NativeSelect> </Form> FrequencySelect an option...AlwaysSometimesNeverPlease select a frequency. --- ## Page: https://react-spectrum.adobe.com/react-aria/NumberField.html # NumberField A number field allows a user to enter a number, and increment or decrement the value using stepper buttons. <table><tbody><tr><th>install</th><td><code>yarn add react-aria-components</code></td></tr><tr><th>version</th><td>1.10.1</td></tr><tr><th>usage</th><td><code><span>import</span> {NumberField} <span>from</span> <span>'react-aria-components'</span></code></td></tr></tbody></table> View ARIA pattern W3C View repository GitHub View package NPM ## Example# * * * import {NumberField, Label, Group, Input, Button} from 'react-aria-components'; <NumberField defaultValue={1024} minValue={0}> <Label>Width</Label> <Group> <Button slot="decrement">-</Button> <Input /> <Button slot="increment">+</Button> </Group> </NumberField> import { Button, Group, Input, Label, NumberField } from 'react-aria-components'; <NumberField defaultValue={1024} minValue={0}> <Label>Width</Label> <Group> <Button slot="decrement">-</Button> <Input /> <Button slot="increment">+</Button> </Group> </NumberField> import { Button, Group, Input, Label, NumberField } from 'react-aria-components'; <NumberField defaultValue={1024} minValue={0} > <Label>Width</Label> <Group> <Button slot="decrement"> - </Button> <Input /> <Button slot="increment"> + </Button> </Group> </NumberField> Width \-+ Show CSS @import "@react-aria/example-theme"; .react-aria-NumberField { margin-bottom: 8px; color: var(--text-color); .react-aria-Group { display: flex; width: fit-content; border-radius: 4px; &[data-focus-within] { outline: 1px solid var(--focus-ring-color); .react-aria-Input, .react-aria-Button { border-color: var(--focus-ring-color); } } } .react-aria-Button { font-size: 1.4rem; width: 2.3rem; padding: 0; &[slot=decrement] { border-start-end-radius: 0; border-end-end-radius: 0; } &[slot=increment] { border-start-start-radius: 0; border-end-start-radius: 0; } } .react-aria-Input { background: var(--field-background); border: 1px solid var(--border-color); border-radius: 0; color: var(--field-text-color); margin: 0 -1px; z-index: 1; font-size: 1rem; padding: 0.429rem 0.571rem; outline: none; width: 6rem; flex: 1; } } @import "@react-aria/example-theme"; .react-aria-NumberField { margin-bottom: 8px; color: var(--text-color); .react-aria-Group { display: flex; width: fit-content; border-radius: 4px; &[data-focus-within] { outline: 1px solid var(--focus-ring-color); .react-aria-Input, .react-aria-Button { border-color: var(--focus-ring-color); } } } .react-aria-Button { font-size: 1.4rem; width: 2.3rem; padding: 0; &[slot=decrement] { border-start-end-radius: 0; border-end-end-radius: 0; } &[slot=increment] { border-start-start-radius: 0; border-end-start-radius: 0; } } .react-aria-Input { background: var(--field-background); border: 1px solid var(--border-color); border-radius: 0; color: var(--field-text-color); margin: 0 -1px; z-index: 1; font-size: 1rem; padding: 0.429rem 0.571rem; outline: none; width: 6rem; flex: 1; } } @import "@react-aria/example-theme"; .react-aria-NumberField { margin-bottom: 8px; color: var(--text-color); .react-aria-Group { display: flex; width: fit-content; border-radius: 4px; &[data-focus-within] { outline: 1px solid var(--focus-ring-color); .react-aria-Input, .react-aria-Button { border-color: var(--focus-ring-color); } } } .react-aria-Button { font-size: 1.4rem; width: 2.3rem; padding: 0; &[slot=decrement] { border-start-end-radius: 0; border-end-end-radius: 0; } &[slot=increment] { border-start-start-radius: 0; border-end-start-radius: 0; } } .react-aria-Input { background: var(--field-background); border: 1px solid var(--border-color); border-radius: 0; color: var(--field-text-color); margin: 0 -1px; z-index: 1; font-size: 1rem; padding: 0.429rem 0.571rem; outline: none; width: 6rem; flex: 1; } } ## Features# * * * Number fields can be built with `<input type="number">`, but the behavior is very inconsistent across browsers and platforms, it supports limited localized formatting options, and it is challenging to style the stepper buttons. `NumberField` helps achieve accessible number fields that support internationalized formatting options and can be styled as needed. * **Customizable formatting** – Support for internationalized number formatting and parsing including decimals, percentages, currency values, and units. Multiple currency formats are supported, including symbol, code, and name in standard or accounting notation. * **International** – Support for the Latin, Arabic, Devanagari, Bengali, and Han positional decimal numbering systems in over 30 locales. The numbering system is automatically detected from user input, and input method editors such as Pinyin are supported. * **Validation** – Keyboard input is validated as the user types so that only numeric input is accepted, according to the locale and numbering system. Values are automatically rounded and clamped according to configurable decimal, minimum, maximum, and step values. Custom client and server-side validation is also supported. * **Mobile friendly** – An appropriate software keyboard is automatically selected based on the allowed values for ease of use. * **Accessible** – Follows the spinbutton ARIA pattern, with support for arrow keys, scroll wheel, and stepper buttons to increment and decrement the value. Extensively tested across many devices and assistive technologies to ensure announcements and behaviors are consistent. Read our blog post for more details about the interactions and internationalization support implemented by `NumberField`. ## Anatomy# * * * Number fields consist of an input element that shows the current value and allows the user to type a new value, optional stepper buttons to increment and decrement the value, a group containing the input and stepper buttons, and a label. `NumberField` also supports optional description and error message elements, which can be used to provide more context about the field, and any validation messages. These are linked with the input via the `aria-describedby` attribute. import {Button, FieldError, Group, Input, Label, NumberField, Text} from 'react-aria-components'; <NumberField> <Label /> <Group> <Input /> <Button slot="increment" /> <Button slot="decrement" /> </Group> <Text slot="description" /> <FieldError /> </NumberField> import { Button, FieldError, Group, Input, Label, NumberField, Text } from 'react-aria-components'; <NumberField> <Label /> <Group> <Input /> <Button slot="increment" /> <Button slot="decrement" /> </Group> <Text slot="description" /> <FieldError /> </NumberField> import { Button, FieldError, Group, Input, Label, NumberField, Text } from 'react-aria-components'; <NumberField> <Label /> <Group> <Input /> <Button slot="increment" /> <Button slot="decrement" /> </Group> <Text slot="description" /> <FieldError /> </NumberField> If there is no visual label, an `aria-label` or `aria-labelledby` prop must be passed instead to identify the element to screen readers. ### Concepts# `NumberField` makes use of the following concepts: Forms Validating and submitting form data, and integrating with form libraries. ### Composed components# A `NumberField` uses the following components, which may also be used standalone or reused in other components. Label A label provides context for an input element. Input An input allows a user to enter a plain text value with a keyboard. Button A button allows a user to perform an action. ## Starter kits# * * * To help kick-start your project, we offer starter kits that include example implementations of all React Aria components with various styling solutions. All components are fully styled, including support for dark mode, high contrast mode, and all UI states. Each starter comes with a pre-configured Storybook that you can experiment with, or use as a starting point for your own component library. Vanilla CSS Download ZIP Preview Tailwind CSS Download ZIP Preview ## Reusable wrappers# * * * If you will use a NumberField in multiple places in your app, you can wrap all of the pieces into a reusable component. This way, the DOM structure, styling code, and other logic are defined in a single place and reused everywhere to ensure consistency. This example wraps `NumberField` and all of its children together into a single component which accepts a `label` prop, which is passed to the right place. It also shows how to use the `description` slot to render help text, and `FieldError` component to render validation errors. import type {NumberFieldProps, ValidationResult} from 'react-aria-components'; import {FieldError, Text} from 'react-aria-components'; interface MyNumberFieldProps extends NumberFieldProps { label?: string; description?: string; errorMessage?: string | ((validation: ValidationResult) => string); } function MyNumberField( { label, description, errorMessage, ...props }: MyNumberFieldProps ) { return ( <NumberField {...props}> <Label>{label}</Label> <Group> <Button slot="decrement">-</Button> <Input /> <Button slot="increment">+</Button> </Group> {description && <Text slot="description">{description}</Text>} <FieldError>{errorMessage}</FieldError> </NumberField> ); } <MyNumberField label="Cookies" /> import type { NumberFieldProps, ValidationResult } from 'react-aria-components'; import {FieldError, Text} from 'react-aria-components'; interface MyNumberFieldProps extends NumberFieldProps { label?: string; description?: string; errorMessage?: | string | ((validation: ValidationResult) => string); } function MyNumberField( { label, description, errorMessage, ...props }: MyNumberFieldProps ) { return ( <NumberField {...props}> <Label>{label}</Label> <Group> <Button slot="decrement">-</Button> <Input /> <Button slot="increment">+</Button> </Group> {description && ( <Text slot="description">{description}</Text> )} <FieldError>{errorMessage}</FieldError> </NumberField> ); } <MyNumberField label="Cookies" /> import type { NumberFieldProps, ValidationResult } from 'react-aria-components'; import { FieldError, Text } from 'react-aria-components'; interface MyNumberFieldProps extends NumberFieldProps { label?: string; description?: string; errorMessage?: | string | (( validation: ValidationResult ) => string); } function MyNumberField({ label, description, errorMessage, ...props }: MyNumberFieldProps) { return ( <NumberField {...props} > <Label> {label} </Label> <Group> <Button slot="decrement"> - </Button> <Input /> <Button slot="increment"> + </Button> </Group> {description && ( <Text slot="description"> {description} </Text> )} <FieldError> {errorMessage} </FieldError> </NumberField> ); } <MyNumberField label="Cookies" /> Cookies \-+ ## Value# * * * ### Controlled# By default, `NumberField` is uncontrolled. However, when using the `value` prop, it becomes controlled. This allows you to store the current value in your own state, and use it elsewhere. The `onChange` event is triggered whenever the number value updates. This happens when the user types a value and blurs the input, or when incrementing or decrementing the value. It does not happen as the user types because partial input may not be parseable to a valid number. function Example() { let [value, setValue] = React.useState(6); return ( <> <MyNumberField label="Controlled value" value={value} onChange={setValue} /> <div>Current value prop: {value}</div> </> ); } function Example() { let [value, setValue] = React.useState(6); return ( <> <MyNumberField label="Controlled value" value={value} onChange={setValue} /> <div>Current value prop: {value}</div> </> ); } function Example() { let [value, setValue] = React.useState(6); return ( <> <MyNumberField label="Controlled value" value={value} onChange={setValue} /> <div> Current value prop: {value} </div> </> ); } Controlled value \-+ Current value prop: 6 ### HTML forms# NumberField supports the `name` prop for integration with HTML forms. The value will be submitted to the server as a raw number (e.g. `45`), not as a locale-dependent formatted string (e.g. `"$45.00"`). <MyNumberField label="Transaction amount" name="amount" defaultValue={45} formatOptions={{ style: 'currency', currency: 'USD' }} /> <MyNumberField label="Transaction amount" name="amount" defaultValue={45} formatOptions={{ style: 'currency', currency: 'USD' }} /> <MyNumberField label="Transaction amount" name="amount" defaultValue={45} formatOptions={{ style: 'currency', currency: 'USD' }} /> Transaction amount \-+ ## Number formatting# * * * ### Decimals# The default formatting style for `NumberField` is decimal, but you can configure various aspects via the `formatOptions` prop. All options supported by Intl.NumberFormat are supported, including configuration of minimum and maximum fraction digits, sign display, grouping separators, etc. Currently only standard notation is supported, though scientific, engineering, and compact notation may be supported in the future. The following example uses the `signDisplay` option to include the plus sign for positive numbers, for example to display an offset from some value. In addition, it always displays a minimum of 1 digit after the decimal point, and allows up to 2 fraction digits. If the user enters more than 2 fraction digits, the result will be rounded. <MyNumberField label="Adjust exposure" defaultValue={0} formatOptions={{ signDisplay: 'exceptZero', minimumFractionDigits: 1, maximumFractionDigits: 2 }} /> <MyNumberField label="Adjust exposure" defaultValue={0} formatOptions={{ signDisplay: 'exceptZero', minimumFractionDigits: 1, maximumFractionDigits: 2 }} /> <MyNumberField label="Adjust exposure" defaultValue={0} formatOptions={{ signDisplay: 'exceptZero', minimumFractionDigits: 1, maximumFractionDigits: 2 }} /> Adjust exposure \-+ ### Percentages# The `style: 'percent'` option can be passed to the `formatOptions` prop to treat the value as a percentage. In this mode, the value is multiplied by 100 before it is displayed, i.e. `0.45` is displayed as `45%`. The reverse is also true: when the user enters a value, the `onChange` event will be triggered with the entered value divided by 100. When the percent option is enabled, the default step automatically changes to `0.01` such that incrementing and decrementing occurs by `1%`. This can be overridden with the `step` prop. See below for details. <MyNumberField label="Sales tax" defaultValue={0.05} formatOptions={{ style: 'percent' }} /> <MyNumberField label="Sales tax" defaultValue={0.05} formatOptions={{ style: 'percent' }} /> <MyNumberField label="Sales tax" defaultValue={0.05} formatOptions={{ style: 'percent' }} /> Sales tax \-+ ### Currency values# The `style: 'currency'` option can be passed to the `formatOptions` prop to treat the value as a currency value. The `currency` option must also be passed to set the currency code (e.g. `USD`) to use. In addition, the `currencyDisplay` option can be used to choose whether to display the currency symbol, currency code, or currency name. Finally, the `currencySign` option can be set to `accounting` to use accounting notation for negative numbers, which uses parentheses rather than a minus sign in some locales. If you need to allow the user to change the currency, you should include a separate dropdown next to the number field. The number field itself will not determine the currency from the user input. <MyNumberField label="Transaction amount" defaultValue={45} formatOptions={{ style: 'currency', currency: 'EUR', currencyDisplay: 'code', currencySign: 'accounting' }} /> <MyNumberField label="Transaction amount" defaultValue={45} formatOptions={{ style: 'currency', currency: 'EUR', currencyDisplay: 'code', currencySign: 'accounting' }} /> <MyNumberField label="Transaction amount" defaultValue={45} formatOptions={{ style: 'currency', currency: 'EUR', currencyDisplay: 'code', currencySign: 'accounting' }} /> Transaction amount \-+ ### Units# The `style: 'unit'` option can be passed to the `formatOptions` prop to format the value with a unit of measurement. The `unit` option must also be passed to set which unit to use (e.g. `inch`). In addition, the `unitDisplay` option can be used to choose whether to display the unit in long, short, or narrow format. If you need to allow the user to change the unit, you should include a separate dropdown next to the number field. The number field itself will not determine the unit from the user input. **Note:** the unit style is not currently supported in Safari. A polyfill may be necessary. <MyNumberField label="Package width" defaultValue={4} formatOptions={{ style: 'unit', unit: 'inch', unitDisplay: 'long' }} /> <MyNumberField label="Package width" defaultValue={4} formatOptions={{ style: 'unit', unit: 'inch', unitDisplay: 'long' }} /> <MyNumberField label="Package width" defaultValue={4} formatOptions={{ style: 'unit', unit: 'inch', unitDisplay: 'long' }} /> Package width \-+ ## Validation# * * * ### Minimum and maximum values# The `minValue` and `maxValue` props can be used to limit the entered value to a specific range. The value will be clamped when the user blurs the input field. In addition, the increment and decrement buttons will be disabled when the value is within one `step` value from the bounds (see below for info about steps). Ranges can be open ended by only providing either `minValue` or `maxValue` rather than both. If a valid range is known ahead of time, it is a good idea to provide it to `NumberField` so it can optimize the experience. For example, when the minimum value is greater than or equal to zero, it is possible to use a numeric keyboard on iOS rather than a full text keyboard (necessary to enter a minus sign). <MyNumberField label="Enter your age" minValue={0} /> <MyNumberField label="Enter your age" minValue={0} /> <MyNumberField label="Enter your age" minValue={0} /> Enter your age \-+ ### Step values# The `step` prop can be used to snap the value to certain increments. If there is a `minValue` defined, the steps are calculated starting from the minimum. For example, if `minValue={2}`, and `step={3}`, the valid step values would be 2, 5, 8, 11, etc. If no `minValue` is defined, the steps are calculated starting from zero and extending in both directions. In other words, such that the values are evenly divisible by the step. If no `step` is defined, any decimal value may be typed, but incrementing and decrementing snaps the value to an integer. If the user types a value that is between two steps and blurs the input, the value will be snapped to the nearest step. When incrementing or decrementing, the value is snapped to the nearest step that is higher or lower, respectively. When incrementing or decrementing from an empty field, the value starts at the `minValue` or `maxValue`, respectively, if defined. Otherwise, the value starts from `0`. <MyNumberField label="Step" step={10} /> <MyNumberField label="Step + minValue" minValue={2} step={3} /> <MyNumberField label="Step + minValue + maxValue" minValue={2} maxValue={21} step={3} /> <MyNumberField label="Step" step={10} /> <MyNumberField label="Step + minValue" minValue={2} step={3} /> <MyNumberField label="Step + minValue + maxValue" minValue={2} maxValue={21} step={3} /> <MyNumberField label="Step" step={10} /> <MyNumberField label="Step + minValue" minValue={2} step={3} /> <MyNumberField label="Step + minValue + maxValue" minValue={2} maxValue={21} step={3} /> Step \-+ Step + minValue \-+ Step + minValue + maxValue \-+ ### Validation errors# NumberField supports the `isRequired` prop to ensure the user enters a value, as well as custom validation functions, realtime validation, and server-side validation. It can also be integrated with other form libraries. See the Forms guide to learn more. To display validation errors, add a `<FieldError>` element as a child of the NumberField. This allows you to render error messages from all of the above sources with consistent custom styles. import {Form, FieldError} from 'react-aria-components'; <Form> <NumberField name="width" isRequired> <Label>Width</Label> <Group> <Button slot="decrement">-</Button> <Input /> <Button slot="increment">+</Button> </Group> <FieldError /> </NumberField> <Button type="submit">Submit</Button> </Form> import {Form, FieldError} from 'react-aria-components'; <Form> <NumberField name="width" isRequired> <Label>Width</Label> <Group> <Button slot="decrement">-</Button> <Input /> <Button slot="increment">+</Button> </Group> <FieldError /> </NumberField> <Button type="submit">Submit</Button> </Form> import { FieldError, Form } from 'react-aria-components'; <Form> <NumberField name="width" isRequired > <Label> Width </Label> <Group> <Button slot="decrement"> - </Button> <Input /> <Button slot="increment"> + </Button> </Group> <FieldError /> </NumberField> <Button type="submit"> Submit </Button> </Form> Width \-+ Submit Show CSS .react-aria-NumberField { &[data-invalid] { .react-aria-Input, .react-aria-Button { border-color: var(--invalid-color); } &:focus-within { .react-aria-Input, .react-aria-Button { border-color: var(--focus-ring-color); } } } .react-aria-FieldError { font-size: 12px; color: var(--invalid-color); } } .react-aria-NumberField { &[data-invalid] { .react-aria-Input, .react-aria-Button { border-color: var(--invalid-color); } &:focus-within { .react-aria-Input, .react-aria-Button { border-color: var(--focus-ring-color); } } } .react-aria-FieldError { font-size: 12px; color: var(--invalid-color); } } .react-aria-NumberField { &[data-invalid] { .react-aria-Input, .react-aria-Button { border-color: var(--invalid-color); } &:focus-within { .react-aria-Input, .react-aria-Button { border-color: var(--focus-ring-color); } } } .react-aria-FieldError { font-size: 12px; color: var(--invalid-color); } } By default, `FieldError` displays default validation messages provided by the browser. See Customizing error messages in the Forms guide to learn how to provide your own custom errors. ### Description# The `description` slot can be used to associate additional help text with a number field. <NumberField> <Label>Width</Label> <Group> <Button slot="decrement">-</Button> <Input /> <Button slot="increment">+</Button> </Group> <Text slot="description">Enter a width in centimeters.</Text></NumberField> <NumberField> <Label>Width</Label> <Group> <Button slot="decrement">-</Button> <Input /> <Button slot="increment">+</Button> </Group> <Text slot="description"> Enter a width in centimeters. </Text></NumberField> <NumberField> <Label>Width</Label> <Group> <Button slot="decrement"> - </Button> <Input /> <Button slot="increment"> + </Button> </Group> <Text slot="description"> Enter a width in centimeters. </Text></NumberField> Width \-+ Enter a width in centimeters. Show CSS .react-aria-NumberField { [slot=description] { font-size: 12px; } } .react-aria-NumberField { [slot=description] { font-size: 12px; } } .react-aria-NumberField { [slot=description] { font-size: 12px; } } ## Disabled# * * * The `isDisabled` prop can be used prevent the user from editing the value of the number field. <MyNumberField label="Disabled" isDisabled value={25} /> <MyNumberField label="Disabled" isDisabled value={25} /> <MyNumberField label="Disabled" isDisabled value={25} /> Disabled \-+ Show CSS .react-aria-NumberField { .react-aria-Button { &[data-disabled] { border-color: var(--border-color-disabled); color: var(--text-color-disabled); } } .react-aria-Input { &[data-disabled] { border-color: var(--border-color-disabled); color: var(--text-color-disabled); } } } .react-aria-NumberField { .react-aria-Button { &[data-disabled] { border-color: var(--border-color-disabled); color: var(--text-color-disabled); } } .react-aria-Input { &[data-disabled] { border-color: var(--border-color-disabled); color: var(--text-color-disabled); } } } .react-aria-NumberField { .react-aria-Button { &[data-disabled] { border-color: var(--border-color-disabled); color: var(--text-color-disabled); } } .react-aria-Input { &[data-disabled] { border-color: var(--border-color-disabled); color: var(--text-color-disabled); } } } ### Read only# The `isReadOnly` prop makes the NumberField's value immutable. Unlike `isDisabled`, the NumberField remains focusable and the contents can still be copied. See the MDN docs for more information. <MyNumberField label="Read only" isReadOnly value={32} /> <MyNumberField label="Read only" isReadOnly value={32} /> <MyNumberField label="Read only" isReadOnly value={32} /> Read only \-+ ## Props# * * * ### NumberField# | Name | Type | Default | Description | | --- | --- | --- | --- | | `decrementAriaLabel` | `string` | — | A custom aria-label for the decrement button. If not provided, the localized string "Decrement" is used. | | `incrementAriaLabel` | `string` | — | A custom aria-label for the increment button. If not provided, the localized string "Increment" is used. | | `isWheelDisabled` | `boolean` | — | Enables or disables changing the value with scroll. | | `formatOptions` | `Intl.NumberFormatOptions` | — | Formatting options for the value displayed in the number field. This also affects what characters are allowed to be typed by the user. | | `isDisabled` | `boolean` | — | Whether the input is disabled. | | `isReadOnly` | `boolean` | — | Whether the input can be selected but not changed by the user. | | `isRequired` | `boolean` | — | Whether user input is required on the input before form submission. | | `isInvalid` | `boolean` | — | Whether the input value is invalid. | | `validate` | `( (value: number )) => ValidationError | true | null | undefined` | — | A function that returns an error message if a given value is invalid. Validation errors are displayed to the user when the form is submitted if `validationBehavior="native"`. For realtime validation, use the `isInvalid` prop instead. | | `autoFocus` | `boolean` | — | Whether the element should receive focus on render. | | `value` | `number` | — | The current value (controlled). | | `defaultValue` | `number` | — | The default value (uncontrolled). | | `minValue` | `number` | — | The smallest value allowed for the input. | | `maxValue` | `number` | — | The largest value allowed for the input. | | `step` | `number` | — | The amount that the input value changes with each increment or decrement "tick". | | `validationBehavior` | `'native' | 'aria'` | `'native'` | Whether to use native HTML form validation to prevent form submission when the value is missing or invalid, or mark the field as required or invalid via ARIA. | | `name` | `string` | — | The name of the input element, used when submitting an HTML form. See MDN. | | `children` | `ReactNode | ( (values: NumberFieldRenderProps & & { defaultChildren: ReactNode | | undefined } )) => ReactNode` | — | The children of the component. A function may be provided to alter the children based on component state. | | `className` | `string | ( (values: NumberFieldRenderProps & & { defaultClassName: string | | undefined } )) => string` | — | The CSS className for the element. A function may be provided to compute the class based on component state. | | `style` | `CSSProperties | ( (values: NumberFieldRenderProps & & { defaultStyle: CSSProperties } )) => CSSProperties | undefined` | — | The inline style for the element. A function may be provided to compute the style based on component state. | Events | Name | Type | Description | | --- | --- | --- | | `onFocus` | `( (e: FocusEvent<Target> )) => void` | Handler that is called when the element receives focus. | | `onBlur` | `( (e: FocusEvent<Target> )) => void` | Handler that is called when the element loses focus. | | `onFocusChange` | `( (isFocused: boolean )) => void` | Handler that is called when the element's focus status changes. | | `onKeyDown` | `( (e: KeyboardEvent )) => void` | Handler that is called when a key is pressed. | | `onKeyUp` | `( (e: KeyboardEvent )) => void` | Handler that is called when a key is released. | | `onChange` | `( (value: T )) => void` | Handler that is called when the value changes. | | `onCopy` | `ClipboardEventHandler<HTMLInputElement>` | Handler that is called when the user copies text. See MDN. | | `onCut` | `ClipboardEventHandler<HTMLInputElement>` | Handler that is called when the user cuts text. See MDN. | | `onPaste` | `ClipboardEventHandler<HTMLInputElement>` | Handler that is called when the user pastes text. See MDN. | | `onCompositionStart` | `CompositionEventHandler<HTMLInputElement>` | Handler that is called when a text composition system starts a new text composition session. See MDN. | | `onCompositionEnd` | `CompositionEventHandler<HTMLInputElement>` | Handler that is called when a text composition system completes or cancels the current text composition session. See MDN. | | `onCompositionUpdate` | `CompositionEventHandler<HTMLInputElement>` | Handler that is called when a new character is received in the current text composition session. See MDN. | | `onSelect` | `ReactEventHandler<HTMLInputElement>` | Handler that is called when text in the input is selected. See MDN. | | `onBeforeInput` | `FormEventHandler<HTMLInputElement>` | Handler that is called when the input value is about to be modified. See MDN. | | `onInput` | `FormEventHandler<HTMLInputElement>` | Handler that is called when the input value is modified. See MDN. | Layout | Name | Type | Description | | --- | --- | --- | | `slot` | `string | null` | A slot name for the component. Slots allow the component to receive props from a parent component. An explicit `null` value indicates that the local props completely override all props received from a parent. | Accessibility | Name | Type | Description | | --- | --- | --- | | `id` | `string` | The element's unique identifier. See MDN. | | `aria-label` | `string` | Defines a string value that labels the current element. | | `aria-labelledby` | `string` | Identifies the element (or elements) that labels the current element. | | `aria-describedby` | `string` | Identifies the element (or elements) that describes the object. | | `aria-details` | `string` | Identifies the element (or elements) that provide a detailed, extended description for the object. | ### Label# A `<Label>` accepts all HTML attributes. ### Group# A `<Group>` accepts all HTML attributes. ### Input# An `<Input>` accepts all props supported by the `<input>` HTML element. ### Button# A `<Button>` accepts its contents as `children`. Other props such as `onPress` and `isDisabled` will be set by the `NumberField`. Show props | Name | Type | Default | Description | | --- | --- | --- | --- | | `form` | `string` | — | The `<form>` element to associate the button with. The value of this attribute must be the id of a `<form>` in the same document. | | `formAction` | `string` | — | The URL that processes the information submitted by the button. Overrides the action attribute of the button's form owner. | | `formEncType` | `string` | — | Indicates how to encode the form data that is submitted. | | `formMethod` | `string` | — | Indicates the HTTP method used to submit the form. | | `formNoValidate` | `boolean` | — | Indicates that the form is not to be validated when it is submitted. | | `formTarget` | `string` | — | Overrides the target attribute of the button's form owner. | | `name` | `string` | — | Submitted as a pair with the button's value as part of the form data. | | `value` | `string` | — | The value associated with the button's name when it's submitted with the form data. | | `isPending` | `boolean` | — | Whether the button is in a pending state. This disables press and hover events while retaining focusability, and announces the pending state to screen readers. | | `isDisabled` | `boolean` | — | Whether the button is disabled. | | `autoFocus` | `boolean` | — | Whether the element should receive focus on render. | | `type` | `'button' | 'submit' | 'reset'` | `'button'` | The behavior of the button when used in an HTML form. | | `children` | `ReactNode | ( (values: ButtonRenderProps & & { defaultChildren: ReactNode | | undefined } )) => ReactNode` | — | The children of the component. A function may be provided to alter the children based on component state. | | `className` | `string | ( (values: ButtonRenderProps & & { defaultClassName: string | | undefined } )) => string` | — | The CSS className for the element. A function may be provided to compute the class based on component state. | | `style` | `CSSProperties | ( (values: ButtonRenderProps & & { defaultStyle: CSSProperties } )) => CSSProperties | undefined` | — | The inline style for the element. A function may be provided to compute the style based on component state. | Events | Name | Type | Description | | --- | --- | --- | | `onPress` | `( (e: PressEvent )) => void` | Handler that is called when the press is released over the target. | | `onPressStart` | `( (e: PressEvent )) => void` | Handler that is called when a press interaction starts. | | `onPressEnd` | `( (e: PressEvent )) => void` | Handler that is called when a press interaction ends, either over the target or when the pointer leaves the target. | | `onPressChange` | `( (isPressed: boolean )) => void` | Handler that is called when the press state changes. | | `onPressUp` | `( (e: PressEvent )) => void` | Handler that is called when a press is released over the target, regardless of whether it started on the target or not. | | `onClick` | `( (e: MouseEvent<FocusableElement> )) => void` | **Not recommended – use `onPress` instead.** `onClick` is an alias for `onPress` provided for compatibility with other libraries. `onPress` provides additional event details for non-mouse interactions. | | `onFocus` | `( (e: FocusEvent<Target> )) => void` | Handler that is called when the element receives focus. | | `onBlur` | `( (e: FocusEvent<Target> )) => void` | Handler that is called when the element loses focus. | | `onFocusChange` | `( (isFocused: boolean )) => void` | Handler that is called when the element's focus status changes. | | `onKeyDown` | `( (e: KeyboardEvent )) => void` | Handler that is called when a key is pressed. | | `onKeyUp` | `( (e: KeyboardEvent )) => void` | Handler that is called when a key is released. | | `onHoverStart` | `( (e: HoverEvent )) => void` | Handler that is called when a hover interaction starts. | | `onHoverEnd` | `( (e: HoverEvent )) => void` | Handler that is called when a hover interaction ends. | | `onHoverChange` | `( (isHovering: boolean )) => void` | Handler that is called when the hover state changes. | Layout | Name | Type | Description | | --- | --- | --- | | `slot` | `string | null` | A slot name for the component. Slots allow the component to receive props from a parent component. An explicit `null` value indicates that the local props completely override all props received from a parent. | Accessibility | Name | Type | Description | | --- | --- | --- | | `id` | `string` | The element's unique identifier. See MDN. | | `excludeFromTabOrder` | `boolean` | Whether to exclude the element from the sequential tab order. If true, the element will not be focusable via the keyboard by tabbing. This should be avoided except in rare scenarios where an alternative means of accessing the element or its functionality via the keyboard is available. | | `preventFocusOnPress` | `boolean` | Whether to prevent focus from moving to the button when pressing it. Caution, this can make the button inaccessible and should only be used when alternative keyboard interaction is provided, such as ComboBox's MenuTrigger or a NumberField's increment/decrement control. | | `aria-expanded` | `boolean | 'true' | 'false'` | Indicates whether the element, or another grouping element it controls, is currently expanded or collapsed. | | `aria-haspopup` | `boolean | 'menu' | 'listbox' | 'tree' | 'grid' | 'dialog' | 'true' | 'false'` | Indicates the availability and type of interactive popup element, such as menu or dialog, that can be triggered by an element. | | `aria-controls` | `string` | Identifies the element (or elements) whose contents or presence are controlled by the current element. | | `aria-pressed` | `boolean | 'true' | 'false' | 'mixed'` | Indicates the current "pressed" state of toggle buttons. | | `aria-current` | `boolean | 'true' | 'false' | 'page' | 'step' | 'location' | 'date' | 'time'` | Indicates whether this element represents the current item within a container or set of related elements. | | `aria-label` | `string` | Defines a string value that labels the current element. | | `aria-labelledby` | `string` | Identifies the element (or elements) that labels the current element. | | `aria-describedby` | `string` | Identifies the element (or elements) that describes the object. | | `aria-details` | `string` | Identifies the element (or elements) that provide a detailed, extended description for the object. | ### FieldError# A `<FieldError>` displays validation errors. Show props | Name | Type | Description | | --- | --- | --- | | `children` | `ReactNode | ( (values: FieldErrorRenderProps & & { defaultChildren: ReactNode | | undefined } )) => ReactNode` | The children of the component. A function may be provided to alter the children based on component state. | | `className` | `string | ( (values: FieldErrorRenderProps & & { defaultClassName: string | | undefined } )) => string` | The CSS className for the element. A function may be provided to compute the class based on component state. | | `style` | `CSSProperties | ( (values: FieldErrorRenderProps & & { defaultStyle: CSSProperties } )) => CSSProperties | undefined` | The inline style for the element. A function may be provided to compute the style based on component state. | Accessibility | Name | Type | Description | | --- | --- | --- | | `id` | `string` | The element's unique identifier. See MDN. | ## Styling# * * * React Aria components can be styled in many ways, including using CSS classes, inline styles, utility classes (e.g. Tailwind), CSS-in-JS (e.g. Styled Components), etc. By default, all components include a builtin `className` attribute which can be targeted using CSS selectors. These follow the `react-aria-ComponentName` naming convention. .react-aria-NumberField { /* ... */ } .react-aria-NumberField { /* ... */ } .react-aria-NumberField { /* ... */ } A custom `className` can also be specified on any component. This overrides the default `className` provided by React Aria with your own. <NumberField className="my-number-field"> {/* ... */} </NumberField> <NumberField className="my-number-field"> {/* ... */} </NumberField> <NumberField className="my-number-field"> {/* ... */} </NumberField> In addition, some components support multiple UI states (e.g. focused, placeholder, readonly, etc.). React Aria components expose states using data attributes, which you can target in CSS selectors. For example: .react-aria-Button[data-pressed] { /* ... */ } .react-aria-Button[data-pressed] { /* ... */ } .react-aria-Button[data-pressed] { /* ... */ } The `className` and `style` props also accept functions which receive states for styling. This lets you dynamically determine the classes or styles to apply, which is useful when using utility CSS libraries like Tailwind. <Button className={({ isPressed }) => isPressed ? 'bg-gray-700' : 'bg-gray-600'} /> <Button className={({ isPressed }) => isPressed ? 'bg-gray-700' : 'bg-gray-600'} /> <Button className={( { isPressed } ) => isPressed ? 'bg-gray-700' : 'bg-gray-600'} /> The states, selectors, and render props for each component used in a `NumberField` are documented below. ### NumberField# A `NumberField` can be targeted with the `.react-aria-NumberField` CSS selector, or by overriding with a custom `className`. It supports the following states: | Name | CSS Selector | Description | | --- | --- | --- | | `isDisabled` | `[data-disabled]` | Whether the number field is disabled. | | `isInvalid` | `[data-invalid]` | Whether the number field is invalid. | | `isRequired` | `[data-required]` | Whether the number field is required. | | `state` | `—` | State of the number field. | ### Label# A `Label` can be targeted with the `.react-aria-Label` CSS selector, or by overriding with a custom `className`. ### Group# A `Group` can be targeted with the `.react-aria-Group` selector, or by overriding with a custom `className`. It supports the following states: | Name | CSS Selector | Description | | --- | --- | --- | | `isHovered` | `[data-hovered]` | Whether the group is currently hovered with a mouse. | | `isFocusWithin` | `[data-focus-within]` | Whether an element within the group is focused, either via a mouse or keyboard. | | `isFocusVisible` | `[data-focus-visible]` | Whether an element within the group is keyboard focused. | | `isDisabled` | `[data-disabled]` | Whether the group is disabled. | | `isInvalid` | `[data-invalid]` | Whether the group is invalid. | ### Input# An `Input` can be targeted with the `.react-aria-Input` CSS selector, or by overriding with a custom `className`. It supports the following states: | Name | CSS Selector | Description | | --- | --- | --- | | `isHovered` | `[data-hovered]` | Whether the input is currently hovered with a mouse. | | `isFocused` | `[data-focused]` | Whether the input is focused, either via a mouse or keyboard. | | `isFocusVisible` | `[data-focus-visible]` | Whether the input is keyboard focused. | | `isDisabled` | `[data-disabled]` | Whether the input is disabled. | | `isInvalid` | `[data-invalid]` | Whether the input is invalid. | ### Button# A Button can be targeted with the `.react-aria-Button` CSS selector, or by overriding with a custom `className`. Within a `NumberField`, there are two slots, which can be targeted with the `[slot=increment]` and `[slot=decrement]` CSS selectors. Buttons support the following states: | Name | CSS Selector | Description | | --- | --- | --- | | `isHovered` | `[data-hovered]` | Whether the button is currently hovered with a mouse. | | `isPressed` | `[data-pressed]` | Whether the button is currently in a pressed state. | | `isFocused` | `[data-focused]` | Whether the button is focused, either via a mouse or keyboard. | | `isFocusVisible` | `[data-focus-visible]` | Whether the button is keyboard focused. | | `isDisabled` | `[data-disabled]` | Whether the button is disabled. | | `isPending` | `[data-pending]` | Whether the button is currently in a pending state. | ### Text# The help text elements within a `NumberField` can be targeted with the `[slot=description]` and `[slot=errorMessage]` CSS selectors, or by adding a custom `className`. ### FieldError# A `FieldError` can be targeted with the `.react-aria-FieldError` CSS selector, or by overriding with a custom `className`. It supports the following render props: | Name | Description | | --- | --- | | `isInvalid` | Whether the input value is invalid. | | `validationErrors` | The current error messages for the input if it is invalid, otherwise an empty array. | | `validationDetails` | The native validation details for the input. | ## Advanced customization# * * * ### Composition# If you need to customize one of the components within a `NumberField`, such as `Label` or `Input`, in many cases you can create a wrapper component. This lets you customize the props passed to the component. function MyInput(props) { return <Input {...props} className="my-input" /> } function MyInput(props) { return <Input {...props} className="my-input" /> } function MyInput(props) { return ( <Input {...props} className="my-input" /> ); } ### Contexts# All React Aria Components export a corresponding context that can be used to send props to them from a parent element. This enables you to build your own compositional APIs similar to those found in React Aria Components itself. You can send any prop or ref via context that you could pass to the corresponding component. The local props and ref on the component are merged with the ones passed via context, with the local props taking precedence (following the rules documented in mergeProps). | Component | Context | Props | Ref | | --- | --- | --- | --- | | `NumberField` | `NumberFieldContext` | ` NumberFieldProps ` | `HTMLDivElement` | This example shows a `FieldGroup` component that renders a group of number fields with a title. The entire group can be marked as read only via the `isReadOnly` prop, which is passed to all child number fields via the `NumberFieldContext` provider. import {NumberFieldContext} from 'react-aria-components'; interface FieldGroupProps { title?: string, children?: React.ReactNode, isReadOnly?: boolean } function FieldGroup({title, children, isReadOnly}: FieldGroupProps) { return ( <fieldset> <legend>{title}</legend> <NumberFieldContext.Provider value={{isReadOnly}}> {children} </NumberFieldContext.Provider> </fieldset> ); } <FieldGroup title="Dimensions" isReadOnly> <MyNumberField label="Width" defaultValue={1024} /> <MyNumberField label="Height" defaultValue={768} /> </FieldGroup> import {NumberFieldContext} from 'react-aria-components'; interface FieldGroupProps { title?: string; children?: React.ReactNode; isReadOnly?: boolean; } function FieldGroup( { title, children, isReadOnly }: FieldGroupProps ) { return ( <fieldset> <legend>{title}</legend> <NumberFieldContext.Provider value={{ isReadOnly }}> {children} </NumberFieldContext.Provider> </fieldset> ); } <FieldGroup title="Dimensions" isReadOnly> <MyNumberField label="Width" defaultValue={1024} /> <MyNumberField label="Height" defaultValue={768} /> </FieldGroup> import {NumberFieldContext} from 'react-aria-components'; interface FieldGroupProps { title?: string; children?: React.ReactNode; isReadOnly?: boolean; } function FieldGroup( { title, children, isReadOnly }: FieldGroupProps ) { return ( <fieldset> <legend> {title} </legend> <NumberFieldContext.Provider value={{ isReadOnly }} > {children} </NumberFieldContext.Provider> </fieldset> ); } <FieldGroup title="Dimensions" isReadOnly > <MyNumberField label="Width" defaultValue={1024} /> <MyNumberField label="Height" defaultValue={768} /> </FieldGroup> Dimensions Width \-+ Height \-+ Show CSS fieldset { padding: 1.5em; width: fit-content; } fieldset { padding: 1.5em; width: fit-content; } fieldset { padding: 1.5em; width: fit-content; } ### Custom children# NumberField passes props to its child components, such as the label and input, via their associated contexts. These contexts are exported so you can also consume them in your own custom components. This enables you to reuse existing components from your app or component library together with React Aria Components. | Component | Context | Props | Ref | | --- | --- | --- | --- | | `Label` | `LabelContext` | ` LabelProps ` | `HTMLLabelElement` | | `Group` | `GroupContext` | ` GroupProps ` | `HTMLDivElement` | | `Input` | `InputContext` | ` InputProps ` | `HTMLInputElement` | | `Button` | `ButtonContext` | ` ButtonProps ` | `HTMLButtonElement` | | `Text` | `TextContext` | ` TextProps ` | `HTMLElement` | This example consumes from `LabelContext` in an existing styled label component to make it compatible with React Aria Components. The `useContextProps` hook merges the local props and ref with the ones provided via context by NumberField. import type {LabelProps} from 'react-aria-components'; import {LabelContext, useContextProps} from 'react-aria-components'; const MyCustomLabel = React.forwardRef( (props: LabelProps, ref: React.ForwardedRef<HTMLLabelElement>) => { // Merge the local props and ref with the ones provided via context. [props, ref] = useContextProps(props, ref, LabelContext); // ... your existing Label component return <label {...props} ref={ref} />; } ); import type {LabelProps} from 'react-aria-components'; import { LabelContext, useContextProps } from 'react-aria-components'; const MyCustomLabel = React.forwardRef( ( props: LabelProps, ref: React.ForwardedRef<HTMLLabelElement> ) => { // Merge the local props and ref with the ones provided via context. [props, ref] = useContextProps( props, ref, LabelContext ); // ... your existing Label component return <label {...props} ref={ref} />; } ); import type {LabelProps} from 'react-aria-components'; import { LabelContext, useContextProps } from 'react-aria-components'; const MyCustomLabel = React.forwardRef( ( props: LabelProps, ref: React.ForwardedRef< HTMLLabelElement > ) => { // Merge the local props and ref with the ones provided via context. [props, ref] = useContextProps( props, ref, LabelContext ); // ... your existing Label component return ( <label {...props} ref={ref} /> ); } ); Now you can use `MyCustomLabel` within a `NumberField`, in place of the builtin React Aria Components `Label`. <NumberField> <MyCustomLabel>Value</MyCustomLabel> <Group> <Button slot="decrement">-</Button> <Input /> <Button slot="increment">+</Button> </Group> </NumberField> <NumberField> <MyCustomLabel>Value</MyCustomLabel> <Group> <Button slot="decrement">-</Button> <Input /> <Button slot="increment">+</Button> </Group> </NumberField> <NumberField> <MyCustomLabel> Value </MyCustomLabel> <Group> <Button slot="decrement"> - </Button> <Input /> <Button slot="increment"> + </Button> </Group> </NumberField> ### State# NumberField provides a `NumberFieldState` object to its children via `NumberFieldStateContext`. This can be used to access and manipulate the NumberField's state. ### Hooks# If you need to customize things even further, such as accessing internal state or customizing DOM structure, you can drop down to the lower level Hook-based API. See useNumberField for more details. --- ## Page: https://react-spectrum.adobe.com/react-aria/RadioGroup.html # RadioGroup A radio group allows a user to select a single item from a list of mutually exclusive options. <table><tbody><tr><th>install</th><td><code>yarn add react-aria-components</code></td></tr><tr><th>version</th><td>1.10.1</td></tr><tr><th>usage</th><td><code><span>import</span> {RadioGroup} <span>from</span> <span>'react-aria-components'</span></code></td></tr></tbody></table> View ARIA pattern W3C View repository GitHub View package NPM ## Example# * * * import {RadioGroup, Radio, Label} from 'react-aria-components'; <RadioGroup> <Label>Favorite pet</Label> <Radio value="dogs">Dog</Radio> <Radio value="cats">Cat</Radio> <Radio value="dragon">Dragon</Radio> </RadioGroup> import { Label, Radio, RadioGroup } from 'react-aria-components'; <RadioGroup> <Label>Favorite pet</Label> <Radio value="dogs">Dog</Radio> <Radio value="cats">Cat</Radio> <Radio value="dragon">Dragon</Radio> </RadioGroup> import { Label, Radio, RadioGroup } from 'react-aria-components'; <RadioGroup> <Label> Favorite pet </Label> <Radio value="dogs"> Dog </Radio> <Radio value="cats"> Cat </Radio> <Radio value="dragon"> Dragon </Radio> </RadioGroup> Favorite petDogCatDragon Show CSS @import "@react-aria/example-theme"; .react-aria-RadioGroup { display: flex; flex-direction: column; gap: 8px; color: var(--text-color); } .react-aria-Radio { display: flex; /* This is needed so the HiddenInput is positioned correctly */ position: relative; align-items: center; gap: 0.571rem; font-size: 1.143rem; color: var(--text-color); forced-color-adjust: none; &:before { content: ''; display: block; width: 1.286rem; height: 1.286rem; box-sizing: border-box; border: 0.143rem solid var(--border-color); background: var(--field-background); border-radius: 1.286rem; transition: all 200ms; } &[data-pressed]:before { border-color: var(--border-color-pressed); } &[data-selected] { &:before { border-color: var(--highlight-background); border-width: 0.429rem; } &[data-pressed]:before { border-color: var(--highlight-background-pressed); } } &[data-focus-visible]:before { outline: 2px solid var(--focus-ring-color); outline-offset: 2px; } } @import "@react-aria/example-theme"; .react-aria-RadioGroup { display: flex; flex-direction: column; gap: 8px; color: var(--text-color); } .react-aria-Radio { display: flex; /* This is needed so the HiddenInput is positioned correctly */ position: relative; align-items: center; gap: 0.571rem; font-size: 1.143rem; color: var(--text-color); forced-color-adjust: none; &:before { content: ''; display: block; width: 1.286rem; height: 1.286rem; box-sizing: border-box; border: 0.143rem solid var(--border-color); background: var(--field-background); border-radius: 1.286rem; transition: all 200ms; } &[data-pressed]:before { border-color: var(--border-color-pressed); } &[data-selected] { &:before { border-color: var(--highlight-background); border-width: 0.429rem; } &[data-pressed]:before { border-color: var(--highlight-background-pressed); } } &[data-focus-visible]:before { outline: 2px solid var(--focus-ring-color); outline-offset: 2px; } } @import "@react-aria/example-theme"; .react-aria-RadioGroup { display: flex; flex-direction: column; gap: 8px; color: var(--text-color); } .react-aria-Radio { display: flex; /* This is needed so the HiddenInput is positioned correctly */ position: relative; align-items: center; gap: 0.571rem; font-size: 1.143rem; color: var(--text-color); forced-color-adjust: none; &:before { content: ''; display: block; width: 1.286rem; height: 1.286rem; box-sizing: border-box; border: 0.143rem solid var(--border-color); background: var(--field-background); border-radius: 1.286rem; transition: all 200ms; } &[data-pressed]:before { border-color: var(--border-color-pressed); } &[data-selected] { &:before { border-color: var(--highlight-background); border-width: 0.429rem; } &[data-pressed]:before { border-color: var(--highlight-background-pressed); } } &[data-focus-visible]:before { outline: 2px solid var(--focus-ring-color); outline-offset: 2px; } } ## Features# * * * Radio groups can be built in HTML with the <fieldset> and <input> elements, however these can be difficult to style. `RadioGroup` and `Radio` help achieve accessible radio groups that can be styled as needed. * **Accessible** – Radio groups are exposed to assistive technology via ARIA, and automatically associate a nested `<Label>`. Description and error message help text slots are supported as well. * **HTML form integration** – Each individual radio uses a visually hidden `<input>` element under the hood, which enables HTML form integration and autofill. * **Validation** – Support for native HTML constraint validation with customizable UI, custom validation functions, and server-side validation errors. * **Cross-browser** – Mouse, touch, keyboard, and focus interactions are normalized to ensure consistency across browsers and devices. ## Anatomy# * * * A radio group consists of a set of radio buttons, and a label. Each radio includes a label and a visual selection indicator. A single radio button within the group can be selected at a time. Users may click or touch a radio button to select it, or use the Tab key to navigate to the group, the arrow keys to navigate within the group, and the Space key to select an option. `RadioGroup` also supports optional description and error message elements, which can be used to provide more context about the field, and any validation messages. These are linked with the inputs via the `aria-describedby` attribute. import {FieldError, Label, Radio, RadioGroup, Text} from 'react-aria-components'; <RadioGroup> <Label /> <Radio /> <Text slot="description" /> <FieldError /> </RadioGroup> import { FieldError, Label, Radio, RadioGroup, Text } from 'react-aria-components'; <RadioGroup> <Label /> <Radio /> <Text slot="description" /> <FieldError /> </RadioGroup> import { FieldError, Label, Radio, RadioGroup, Text } from 'react-aria-components'; <RadioGroup> <Label /> <Radio /> <Text slot="description" /> <FieldError /> </RadioGroup> Individual radio buttons must have a visual label. If the radio group does not have a visible label, an `aria-label` or `aria-labelledby` prop must be passed instead to identify the element to assistive technology. ### Concepts# `RadioGroup` makes use of the following concepts: Forms Validating and submitting form data, and integrating with form libraries. ### Composed components# A `RadioGroup` uses the following components, which may also be used standalone or reused in other components. Label A label provides context for an input element. ## Examples# * * * Shipping Radio Group A shipping options RadioGroup styled with Tailwind CSS. ## Starter kits# * * * To help kick-start your project, we offer starter kits that include example implementations of all React Aria components with various styling solutions. All components are fully styled, including support for dark mode, high contrast mode, and all UI states. Each starter comes with a pre-configured Storybook that you can experiment with, or use as a starting point for your own component library. Vanilla CSS Download ZIP Preview Tailwind CSS Download ZIP Preview ## Reusable wrappers# * * * If you will use a RadioGroup in multiple places in your app, you can wrap all of the pieces into a reusable component. This way, the DOM structure, styling code, and other logic are defined in a single place and reused everywhere to ensure consistency. This example wraps `RadioGroup` and all of its children together into a single component which accepts a `label` prop, which is passed to the right place. It also shows how to use the `description` slot to render help text, and `FieldError` component to render validation errors. import type {RadioGroupProps, ValidationResult} from 'react-aria-components'; import {Text, FieldError} from 'react-aria-components'; interface MyRadioGroupProps extends Omit<RadioGroupProps, 'children'> { children?: React.ReactNode, label?: string, description?: string, errorMessage?: string | ((validation: ValidationResult) => string) } function MyRadioGroup({ label, description, errorMessage, children, ...props }: MyRadioGroupProps) { return ( <RadioGroup {...props}> <Label>{label}</Label> {children} {description && <Text slot="description">{description}</Text>} <FieldError>{errorMessage}</FieldError> </RadioGroup> ); } <MyRadioGroup label="Favorite sport"> <Radio value="soccer">Soccer</Radio> <Radio value="baseball">Baseball</Radio> <Radio value="basketball">Basketball</Radio> </MyRadioGroup> import type { RadioGroupProps, ValidationResult } from 'react-aria-components'; import {FieldError, Text} from 'react-aria-components'; interface MyRadioGroupProps extends Omit<RadioGroupProps, 'children'> { children?: React.ReactNode; label?: string; description?: string; errorMessage?: | string | ((validation: ValidationResult) => string); } function MyRadioGroup({ label, description, errorMessage, children, ...props }: MyRadioGroupProps) { return ( <RadioGroup {...props}> <Label>{label}</Label> {children} {description && ( <Text slot="description">{description}</Text> )} <FieldError>{errorMessage}</FieldError> </RadioGroup> ); } <MyRadioGroup label="Favorite sport"> <Radio value="soccer">Soccer</Radio> <Radio value="baseball">Baseball</Radio> <Radio value="basketball">Basketball</Radio> </MyRadioGroup> import type { RadioGroupProps, ValidationResult } from 'react-aria-components'; import { FieldError, Text } from 'react-aria-components'; interface MyRadioGroupProps extends Omit< RadioGroupProps, 'children' > { children?: React.ReactNode; label?: string; description?: string; errorMessage?: | string | (( validation: ValidationResult ) => string); } function MyRadioGroup({ label, description, errorMessage, children, ...props }: MyRadioGroupProps) { return ( <RadioGroup {...props} > <Label> {label} </Label> {children} {description && ( <Text slot="description"> {description} </Text> )} <FieldError> {errorMessage} </FieldError> </RadioGroup> ); } <MyRadioGroup label="Favorite sport"> <Radio value="soccer"> Soccer </Radio> <Radio value="baseball"> Baseball </Radio> <Radio value="basketball"> Basketball </Radio> </MyRadioGroup> Favorite sportSoccerBaseballBasketball ## Value# * * * ### Default value# An initial, uncontrolled value can be provided to the RadioGroup using the `defaultValue` prop, which accepts a value corresponding with the `value` prop of each Radio. <MyRadioGroup label="Are you a wizard?" defaultValue="yes"> <Radio value="yes">Yes</Radio> <Radio value="no">No</Radio> </MyRadioGroup> <MyRadioGroup label="Are you a wizard?" defaultValue="yes"> <Radio value="yes">Yes</Radio> <Radio value="no">No</Radio> </MyRadioGroup> <MyRadioGroup label="Are you a wizard?" defaultValue="yes" > <Radio value="yes"> Yes </Radio> <Radio value="no"> No </Radio> </MyRadioGroup> Are you a wizard?YesNo ### Controlled value# A controlled value can be provided using the `value` prop, which accepts a value corresponding with the `value` prop of each Radio. The `onChange` event is fired when the user selects a radio. function Example() { let [selected, setSelected] = React.useState(null); return ( <> <MyRadioGroup label="Favorite avatar" value={selected} onChange={setSelected} > <Radio value="wizard">Wizard</Radio> <Radio value="dragon">Dragon</Radio> </MyRadioGroup> <p>You have selected: {selected ?? ''}</p> </> ); } function Example() { let [selected, setSelected] = React.useState(null); return ( <> <MyRadioGroup label="Favorite avatar" value={selected} onChange={setSelected} > <Radio value="wizard">Wizard</Radio> <Radio value="dragon">Dragon</Radio> </MyRadioGroup> <p>You have selected: {selected ?? ''}</p> </> ); } function Example() { let [ selected, setSelected ] = React.useState( null ); return ( <> <MyRadioGroup label="Favorite avatar" value={selected} onChange={setSelected} > <Radio value="wizard"> Wizard </Radio> <Radio value="dragon"> Dragon </Radio> </MyRadioGroup> <p> You have selected:{' '} {selected ?? ''} </p> </> ); } Favorite avatarWizardDragon You have selected: ### HTML forms# RadioGroup supports the `name` prop, paired with the Radio `value` prop, for integration with HTML forms. <MyRadioGroup label="Favorite pet" name="pet"> <Radio value="dogs">Dogs</Radio> <Radio value="cats">Cats</Radio> </MyRadioGroup> <MyRadioGroup label="Favorite pet" name="pet"> <Radio value="dogs">Dogs</Radio> <Radio value="cats">Cats</Radio> </MyRadioGroup> <MyRadioGroup label="Favorite pet" name="pet" > <Radio value="dogs"> Dogs </Radio> <Radio value="cats"> Cats </Radio> </MyRadioGroup> Favorite petDogsCats ## Validation# * * * RadioGroup supports the `isRequired` prop to ensure the user selects an option, as well as custom client and server-side validation. It can also be integrated with other form libraries. See the Forms guide to learn more. To display validation errors, add a `<FieldError>` element as a child of the RadioGroup. This allows you to render error messages from all of the above sources with consistent custom styles. import {Form, FieldError, Button} from 'react-aria-components'; <Form> <RadioGroup name="pet" isRequired> <Label>Favorite pet</Label> <Radio value="dogs">Dog</Radio> <Radio value="cats">Cat</Radio> <Radio value="dragon">Dragon</Radio> <FieldError /> </RadioGroup> <Button type="submit">Submit</Button> </Form> import { Button, FieldError, Form } from 'react-aria-components'; <Form> <RadioGroup name="pet" isRequired> <Label>Favorite pet</Label> <Radio value="dogs">Dog</Radio> <Radio value="cats">Cat</Radio> <Radio value="dragon">Dragon</Radio> <FieldError /> </RadioGroup> <Button type="submit">Submit</Button> </Form> import { Button, FieldError, Form } from 'react-aria-components'; <Form> <RadioGroup name="pet" isRequired > <Label> Favorite pet </Label> <Radio value="dogs"> Dog </Radio> <Radio value="cats"> Cat </Radio> <Radio value="dragon"> Dragon </Radio> <FieldError /> </RadioGroup> <Button type="submit"> Submit </Button> </Form> Favorite petDogCatDragon Submit Show CSS .react-aria-Radio { &[data-invalid] { &:before { border-color: var(--invalid-color); } &[data-pressed]:before { border-color: var(--invalid-color-pressed); } } } .react-aria-RadioGroup { .react-aria-FieldError { font-size: 12px; color: var(--invalid-color); } } .react-aria-Radio { &[data-invalid] { &:before { border-color: var(--invalid-color); } &[data-pressed]:before { border-color: var(--invalid-color-pressed); } } } .react-aria-RadioGroup { .react-aria-FieldError { font-size: 12px; color: var(--invalid-color); } } .react-aria-Radio { &[data-invalid] { &:before { border-color: var(--invalid-color); } &[data-pressed]:before { border-color: var(--invalid-color-pressed); } } } .react-aria-RadioGroup { .react-aria-FieldError { font-size: 12px; color: var(--invalid-color); } } By default, `FieldError` displays default validation messages provided by the browser. See Customizing error messages in the Forms guide to learn how to provide your own custom errors. ### Description# The `description` slot can be used to associate additional help text with a radio group. <RadioGroup> <Label>Favorite avatar</Label> <Radio value="wizard">Wizard</Radio> <Radio value="dragon">Dragon</Radio> <Text slot="description">Please select an avatar.</Text> </RadioGroup> <RadioGroup> <Label>Favorite avatar</Label> <Radio value="wizard">Wizard</Radio> <Radio value="dragon">Dragon</Radio> <Text slot="description">Please select an avatar.</Text> </RadioGroup> <RadioGroup> <Label> Favorite avatar </Label> <Radio value="wizard"> Wizard </Radio> <Radio value="dragon"> Dragon </Radio> <Text slot="description"> Please select an avatar. </Text> </RadioGroup> Favorite avatarWizardDragonPlease select an avatar. Show CSS .react-aria-RadioGroup { [slot=description] { font-size: 12px; } } .react-aria-RadioGroup { [slot=description] { font-size: 12px; } } .react-aria-RadioGroup { [slot=description] { font-size: 12px; } } ## Orientation# * * * RadioGroups are vertically oriented by default. The `orientation` prop can be used to change the orientation to horizontal. <MyRadioGroup label="Favorite avatar" orientation="horizontal"> <Radio value="wizard">Wizard</Radio> <Radio value="dragon">Dragon</Radio> </MyRadioGroup> <MyRadioGroup label="Favorite avatar" orientation="horizontal" > <Radio value="wizard">Wizard</Radio> <Radio value="dragon">Dragon</Radio> </MyRadioGroup> <MyRadioGroup label="Favorite avatar" orientation="horizontal" > <Radio value="wizard"> Wizard </Radio> <Radio value="dragon"> Dragon </Radio> </MyRadioGroup> Favorite avatarWizardDragon Show CSS .react-aria-RadioGroup { &[data-orientation=horizontal] { flex-direction: row; align-items: center; } } .react-aria-RadioGroup { &[data-orientation=horizontal] { flex-direction: row; align-items: center; } } .react-aria-RadioGroup { &[data-orientation=horizontal] { flex-direction: row; align-items: center; } } ## Disabled# * * * The entire RadioGroup can be disabled with the `isDisabled` prop. <MyRadioGroup label="Favorite sport" isDisabled> <Radio value="soccer">Soccer</Radio> <Radio value="baseball">Baseball</Radio> <Radio value="basketball">Basketball</Radio> </MyRadioGroup> <MyRadioGroup label="Favorite sport" isDisabled> <Radio value="soccer">Soccer</Radio> <Radio value="baseball">Baseball</Radio> <Radio value="basketball">Basketball</Radio> </MyRadioGroup> <MyRadioGroup label="Favorite sport" isDisabled > <Radio value="soccer"> Soccer </Radio> <Radio value="baseball"> Baseball </Radio> <Radio value="basketball"> Basketball </Radio> </MyRadioGroup> Favorite sportSoccerBaseballBasketball Show CSS .react-aria-Radio { &[data-disabled] { color: var(--text-color-disabled); &:before { border-color: var(--border-color-disabled); } } } .react-aria-Radio { &[data-disabled] { color: var(--text-color-disabled); &:before { border-color: var(--border-color-disabled); } } } .react-aria-Radio { &[data-disabled] { color: var(--text-color-disabled); &:before { border-color: var(--border-color-disabled); } } } To disable an individual radio, pass `isDisabled` to the `Radio` instead. <MyRadioGroup label="Favorite sport"> <Radio value="soccer">Soccer</Radio> <Radio value="baseball" isDisabled>Baseball</Radio> <Radio value="basketball">Basketball</Radio> </MyRadioGroup> <MyRadioGroup label="Favorite sport"> <Radio value="soccer">Soccer</Radio> <Radio value="baseball" isDisabled>Baseball</Radio> <Radio value="basketball">Basketball</Radio> </MyRadioGroup> <MyRadioGroup label="Favorite sport"> <Radio value="soccer"> Soccer </Radio> <Radio value="baseball" isDisabled > Baseball </Radio> <Radio value="basketball"> Basketball </Radio> </MyRadioGroup> Favorite sportSoccerBaseballBasketball ### Read only# The `isReadOnly` prop makes the selection immutable. Unlike `isDisabled`, the RadioGroup remains focusable. See the MDN docs for more information. <MyRadioGroup label="Favorite avatar" defaultValue="wizard" isReadOnly> <Radio value="wizard">Wizard</Radio> <Radio value="dragon">Dragon</Radio> </MyRadioGroup> <MyRadioGroup label="Favorite avatar" defaultValue="wizard" isReadOnly > <Radio value="wizard">Wizard</Radio> <Radio value="dragon">Dragon</Radio> </MyRadioGroup> <MyRadioGroup label="Favorite avatar" defaultValue="wizard" isReadOnly > <Radio value="wizard"> Wizard </Radio> <Radio value="dragon"> Dragon </Radio> </MyRadioGroup> Favorite avatarWizardDragon ## Props# * * * ### RadioGroup# | Name | Type | Default | Description | | --- | --- | --- | --- | | `orientation` | ` Orientation ` | `'vertical'` | The axis the Radio Button(s) should align with. | | `value` | `string | null` | — | The current value (controlled). | | `defaultValue` | `string | null` | — | The default value (uncontrolled). | | `isDisabled` | `boolean` | — | Whether the input is disabled. | | `isReadOnly` | `boolean` | — | Whether the input can be selected but not changed by the user. | | `name` | `string` | — | The name of the input element, used when submitting an HTML form. See MDN. | | `isRequired` | `boolean` | — | Whether user input is required on the input before form submission. | | `isInvalid` | `boolean` | — | Whether the input value is invalid. | | `validate` | `( (value: string | | null )) => ValidationError | true | null | undefined` | — | A function that returns an error message if a given value is invalid. Validation errors are displayed to the user when the form is submitted if `validationBehavior="native"`. For realtime validation, use the `isInvalid` prop instead. | | `validationBehavior` | `'native' | 'aria'` | `'native'` | Whether to use native HTML form validation to prevent form submission when the value is missing or invalid, or mark the field as required or invalid via ARIA. | | `children` | `ReactNode | ( (values: RadioGroupRenderProps & & { defaultChildren: ReactNode | | undefined } )) => ReactNode` | — | The children of the component. A function may be provided to alter the children based on component state. | | `className` | `string | ( (values: RadioGroupRenderProps & & { defaultClassName: string | | undefined } )) => string` | — | The CSS className for the element. A function may be provided to compute the class based on component state. | | `style` | `CSSProperties | ( (values: RadioGroupRenderProps & & { defaultStyle: CSSProperties } )) => CSSProperties | undefined` | — | The inline style for the element. A function may be provided to compute the style based on component state. | Events | Name | Type | Description | | --- | --- | --- | | `onChange` | `( (value: string )) => void` | Handler that is called when the value changes. | | `onFocus` | `( (e: FocusEvent<Target> )) => void` | Handler that is called when the element receives focus. | | `onBlur` | `( (e: FocusEvent<Target> )) => void` | Handler that is called when the element loses focus. | | `onFocusChange` | `( (isFocused: boolean )) => void` | Handler that is called when the element's focus status changes. | Layout | Name | Type | Description | | --- | --- | --- | | `slot` | `string | null` | A slot name for the component. Slots allow the component to receive props from a parent component. An explicit `null` value indicates that the local props completely override all props received from a parent. | Accessibility | Name | Type | Description | | --- | --- | --- | | `id` | `string` | The element's unique identifier. See MDN. | | `aria-label` | `string` | Defines a string value that labels the current element. | | `aria-labelledby` | `string` | Identifies the element (or elements) that labels the current element. | | `aria-describedby` | `string` | Identifies the element (or elements) that describes the object. | | `aria-details` | `string` | Identifies the element (or elements) that provide a detailed, extended description for the object. | | `aria-errormessage` | `string` | Identifies the element that provides an error message for the object. | ### Radio# | Name | Type | Description | | --- | --- | --- | | `value` | `string` | The value of the radio button, used when submitting an HTML form. See MDN. | | `inputRef` | ` RefObject <HTMLInputElement | null>` | A ref for the HTML input element. | | `isDisabled` | `boolean` | Whether the radio button is disabled or not. Shows that a selection exists, but is not available in that circumstance. | | `autoFocus` | `boolean` | Whether the element should receive focus on render. | | `children` | `ReactNode | ( (values: RadioRenderProps & & { defaultChildren: ReactNode | | undefined } )) => ReactNode` | The children of the component. A function may be provided to alter the children based on component state. | | `className` | `string | ( (values: RadioRenderProps & & { defaultClassName: string | | undefined } )) => string` | The CSS className for the element. A function may be provided to compute the class based on component state. | | `style` | `CSSProperties | ( (values: RadioRenderProps & & { defaultStyle: CSSProperties } )) => CSSProperties | undefined` | The inline style for the element. A function may be provided to compute the style based on component state. | Events | Name | Type | Description | | --- | --- | --- | | `onFocus` | `( (e: FocusEvent<Target> )) => void` | Handler that is called when the element receives focus. | | `onBlur` | `( (e: FocusEvent<Target> )) => void` | Handler that is called when the element loses focus. | | `onFocusChange` | `( (isFocused: boolean )) => void` | Handler that is called when the element's focus status changes. | | `onKeyDown` | `( (e: KeyboardEvent )) => void` | Handler that is called when a key is pressed. | | `onKeyUp` | `( (e: KeyboardEvent )) => void` | Handler that is called when a key is released. | | `onHoverStart` | `( (e: HoverEvent )) => void` | Handler that is called when a hover interaction starts. | | `onHoverEnd` | `( (e: HoverEvent )) => void` | Handler that is called when a hover interaction ends. | | `onHoverChange` | `( (isHovering: boolean )) => void` | Handler that is called when the hover state changes. | Layout | Name | Type | Description | | --- | --- | --- | | `slot` | `string | null` | A slot name for the component. Slots allow the component to receive props from a parent component. An explicit `null` value indicates that the local props completely override all props received from a parent. | Accessibility | Name | Type | Description | | --- | --- | --- | | `id` | `string` | The element's unique identifier. See MDN. | | `aria-label` | `string` | Defines a string value that labels the current element. | | `aria-labelledby` | `string` | Identifies the element (or elements) that labels the current element. | | `aria-describedby` | `string` | Identifies the element (or elements) that describes the object. | | `aria-details` | `string` | Identifies the element (or elements) that provide a detailed, extended description for the object. | ### Text# `<Text>` accepts all HTML attributes. ### FieldError# A `<FieldError>` displays validation errors. Show props | Name | Type | Description | | --- | --- | --- | | `children` | `ReactNode | ( (values: FieldErrorRenderProps & & { defaultChildren: ReactNode | | undefined } )) => ReactNode` | The children of the component. A function may be provided to alter the children based on component state. | | `className` | `string | ( (values: FieldErrorRenderProps & & { defaultClassName: string | | undefined } )) => string` | The CSS className for the element. A function may be provided to compute the class based on component state. | | `style` | `CSSProperties | ( (values: FieldErrorRenderProps & & { defaultStyle: CSSProperties } )) => CSSProperties | undefined` | The inline style for the element. A function may be provided to compute the style based on component state. | Accessibility | Name | Type | Description | | --- | --- | --- | | `id` | `string` | The element's unique identifier. See MDN. | ## Styling# * * * React Aria components can be styled in many ways, including using CSS classes, inline styles, utility classes (e.g. Tailwind), CSS-in-JS (e.g. Styled Components), etc. By default, all components include a builtin `className` attribute which can be targeted using CSS selectors. These follow the `react-aria-ComponentName` naming convention. .react-aria-Radio { /* ... */ } .react-aria-Radio { /* ... */ } .react-aria-Radio { /* ... */ } A custom `className` can also be specified on any component. This overrides the default `className` provided by React Aria with your own. <Radio className="my-radio"> {/* ... */} </Radio> <Radio className="my-radio"> {/* ... */} </Radio> <Radio className="my-radio"> {/* ... */} </Radio> In addition, some components support multiple UI states (e.g. focused, placeholder, readonly, etc.). React Aria components expose states using data attributes, which you can target in CSS selectors. For example: .react-aria-Radio[data-selected] { /* ... */ } .react-aria-Radio[data-selected] { /* ... */ } .react-aria-Radio[data-selected] { /* ... */ } The `className` and `style` props also accept functions which receive states for styling. This lets you dynamically determine the classes or styles to apply, which is useful when using utility CSS libraries like Tailwind. <Radio className={({ isPressed }) => isPressed ? 'bg-gray-700' : 'bg-gray-600'} /> <Radio className={({ isPressed }) => isPressed ? 'bg-gray-700' : 'bg-gray-600'} /> <Radio className={( { isPressed } ) => isPressed ? 'bg-gray-700' : 'bg-gray-600'} /> Render props may also be used as children to alter what elements are rendered based on the current state. For example, you could render an extra element when the radio is selected. <Radio> {({isSelected}) => ( <> {isSelected && <SelectedIcon />} Option </> )} </Radio> <Radio> {({isSelected}) => ( <> {isSelected && <SelectedIcon />} Option </> )} </Radio> <Radio> {( { isSelected } ) => ( <> {isSelected && ( <SelectedIcon /> )} Option </> )} </Radio> The states and selectors for each component used in a `RadioGroup` are documented below. ### RadioGroup# A `RadioGroup` can be targeted with the `.react-aria-RadioGroup` CSS selector, or by overriding with a custom `className`. It supports the following states and render props: | Name | CSS Selector | Description | | --- | --- | --- | | `orientation` | `[data-orientation="horizontal | vertical"]` | The orientation of the radio group. | | `isDisabled` | `[data-disabled]` | Whether the radio group is disabled. | | `isReadOnly` | `[data-readonly]` | Whether the radio group is read only. | | `isRequired` | `[data-required]` | Whether the radio group is required. | | `isInvalid` | `[data-invalid]` | Whether the radio group is invalid. | | `state` | `—` | State of the radio group. | ### Radio# A `Radio` can be targeted with the `.react-aria-Radio` CSS selector, or by overriding with a custom `className`. It supports the following states: | Name | CSS Selector | Description | | --- | --- | --- | | `isSelected` | `[data-selected]` | Whether the radio is selected. | | `isHovered` | `[data-hovered]` | Whether the radio is currently hovered with a mouse. | | `isPressed` | `[data-pressed]` | Whether the radio is currently in a pressed state. | | `isFocused` | `[data-focused]` | Whether the radio is focused, either via a mouse or keyboard. | | `isFocusVisible` | `[data-focus-visible]` | Whether the radio is keyboard focused. | | `isDisabled` | `[data-disabled]` | Whether the radio is disabled. | | `isReadOnly` | `[data-readonly]` | Whether the radio is read only. | | `isInvalid` | `[data-invalid]` | Whether the radio is invalid. | | `isRequired` | `[data-required]` | Whether the checkbox is required. | ### Text# The help text elements within a `RadioGroup` can be targeted with the `[slot=description]` and `[slot=errorMessage]` CSS selectors, or by adding a custom `className`. ### FieldError# A `FieldError` can be targeted with the `.react-aria-FieldError` CSS selector, or by overriding with a custom `className`. It supports the following render props: | Name | Description | | --- | --- | | `isInvalid` | Whether the input value is invalid. | | `validationErrors` | The current error messages for the input if it is invalid, otherwise an empty array. | | `validationDetails` | The native validation details for the input. | ## Advanced customization# * * * ### Contexts# All React Aria Components export a corresponding context that can be used to send props to them from a parent element. This enables you to build your own compositional APIs similar to those found in React Aria Components itself. You can send any prop or ref via context that you could pass to the corresponding component. The local props and ref on the component are merged with the ones passed via context, with the local props taking precedence (following the rules documented in mergeProps). | Component | Context | Props | Ref | | --- | --- | --- | --- | | `RadioGroup` | `RadioGroupContext` | ` RadioGroupProps ` | `HTMLDivElement` | | `Radio` | `RadioContext` | ` RadioProps ` | `HTMLLabelElement` | This example shows a `RadioDescription` component that accepts a radio in its children and renders a description element below it. It uses the useId hook to generate a unique id for the description, and associates it with the radio via the `aria-describedby` attribute passed to the `RadioContext` provider. import {RadioContext} from 'react-aria-components'; import {useId} from 'react-aria'; interface RadioDescriptionProps { children?: React.ReactNode; description?: string; } function RadioDescription({ children, description }: RadioDescriptionProps) { let descriptionId = useId(); return ( <div> <RadioContext.Provider value={{ 'aria-describedby': descriptionId }}> {children} </RadioContext.Provider> <small id={descriptionId}>{description}</small> </div> ); } <MyRadioGroup label="Show scrollbars" defaultValue="automatic"> <RadioDescription description="Scrollbars will always be visible when using a mouse, and only while scrolling when using a trackpad."> <Radio value="automatic">Automatic</Radio> </RadioDescription> <RadioDescription description="Scrollbars will appear only while you are scrolling."> <Radio value="scrolling">While scrolling</Radio> </RadioDescription> <RadioDescription description="Scrollbars will always be visible."> <Radio value="always">Always</Radio> </RadioDescription> </MyRadioGroup> import {RadioContext} from 'react-aria-components'; import {useId} from 'react-aria'; interface RadioDescriptionProps { children?: React.ReactNode; description?: string; } function RadioDescription( { children, description }: RadioDescriptionProps ) { let descriptionId = useId(); return ( <div> <RadioContext.Provider value={{ 'aria-describedby': descriptionId }} > {children} </RadioContext.Provider> <small id={descriptionId}>{description}</small> </div> ); } <MyRadioGroup label="Show scrollbars" defaultValue="automatic" > <RadioDescription description="Scrollbars will always be visible when using a mouse, and only while scrolling when using a trackpad."> <Radio value="automatic">Automatic</Radio> </RadioDescription> <RadioDescription description="Scrollbars will appear only while you are scrolling."> <Radio value="scrolling">While scrolling</Radio> </RadioDescription> <RadioDescription description="Scrollbars will always be visible."> <Radio value="always">Always</Radio> </RadioDescription> </MyRadioGroup> import {RadioContext} from 'react-aria-components'; import {useId} from 'react-aria'; interface RadioDescriptionProps { children?: React.ReactNode; description?: string; } function RadioDescription( { children, description }: RadioDescriptionProps ) { let descriptionId = useId(); return ( <div> <RadioContext.Provider value={{ 'aria-describedby': descriptionId }} > {children} </RadioContext.Provider> <small id={descriptionId} > {description} </small> </div> ); } <MyRadioGroup label="Show scrollbars" defaultValue="automatic" > <RadioDescription description="Scrollbars will always be visible when using a mouse, and only while scrolling when using a trackpad."> <Radio value="automatic"> Automatic </Radio> </RadioDescription> <RadioDescription description="Scrollbars will appear only while you are scrolling."> <Radio value="scrolling"> While scrolling </Radio> </RadioDescription> <RadioDescription description="Scrollbars will always be visible."> <Radio value="always"> Always </Radio> </RadioDescription> </MyRadioGroup> Show scrollbars AutomaticScrollbars will always be visible when using a mouse, and only while scrolling when using a trackpad. While scrollingScrollbars will appear only while you are scrolling. AlwaysScrollbars will always be visible. ### Custom children# RadioGroup passes props to its child components, such as the label, via their associated contexts. These contexts are exported so you can also consume them in your own custom components. This enables you to reuse existing components from your app or component library together with React Aria Components. | Component | Context | Props | Ref | | --- | --- | --- | --- | | `Label` | `LabelContext` | ` LabelProps ` | `HTMLLabelElement` | | `Text` | `TextContext` | ` TextProps ` | `HTMLElement` | This example consumes from `LabelContext` in an existing styled label component to make it compatible with React Aria Components. The `useContextProps` hook merges the local props and ref with the ones provided via context by RadioGroup. import type {LabelProps} from 'react-aria-components'; import {LabelContext, useContextProps} from 'react-aria-components'; const MyCustomLabel = React.forwardRef( (props: LabelProps, ref: React.ForwardedRef<HTMLLabelElement>) => { // Merge the local props and ref with the ones provided via context. [props, ref] = useContextProps(props, ref, LabelContext); // ... your existing Label component return <label {...props} ref={ref} />; } ); import type {LabelProps} from 'react-aria-components'; import { LabelContext, useContextProps } from 'react-aria-components'; const MyCustomLabel = React.forwardRef( ( props: LabelProps, ref: React.ForwardedRef<HTMLLabelElement> ) => { // Merge the local props and ref with the ones provided via context. [props, ref] = useContextProps( props, ref, LabelContext ); // ... your existing Label component return <label {...props} ref={ref} />; } ); import type {LabelProps} from 'react-aria-components'; import { LabelContext, useContextProps } from 'react-aria-components'; const MyCustomLabel = React.forwardRef( ( props: LabelProps, ref: React.ForwardedRef< HTMLLabelElement > ) => { // Merge the local props and ref with the ones provided via context. [props, ref] = useContextProps( props, ref, LabelContext ); // ... your existing Label component return ( <label {...props} ref={ref} /> ); } ); Now you can use `MyCustomLabel` within a `RadioGroup`, in place of the builtin React Aria Components `Label`. <RadioGroup> <MyCustomLabel>Favorite pet</MyCustomLabel> <Radio value="dogs">Dog</Radio> <Radio value="cats">Cat</Radio> <Radio value="dragon">Dragon</Radio> </RadioGroup> <RadioGroup> <MyCustomLabel>Favorite pet</MyCustomLabel> <Radio value="dogs">Dog</Radio> <Radio value="cats">Cat</Radio> <Radio value="dragon">Dragon</Radio> </RadioGroup> <RadioGroup> <MyCustomLabel> Favorite pet </MyCustomLabel> <Radio value="dogs"> Dog </Radio> <Radio value="cats"> Cat </Radio> <Radio value="dragon"> Dragon </Radio> </RadioGroup> ### State# RadioGroup provides a `RadioGroupState` object to its children via `RadioGroupStateContext`. This can be used to access and manipulate the radio group's state. ### Hooks# If you need to customize things further, such as accessing internal state or customizing DOM structure, you can drop down to the lower level Hook-based API. See useRadioGroup for more details. --- ## Page: https://react-spectrum.adobe.com/react-aria/SearchField.html # SearchField A search field allows a user to enter and clear a search query. <table><tbody><tr><th>install</th><td><code>yarn add react-aria-components</code></td></tr><tr><th>version</th><td>1.10.1</td></tr><tr><th>usage</th><td><code><span>import</span> {SearchField} <span>from</span> <span>'react-aria-components'</span></code></td></tr></tbody></table> View ARIA pattern W3C View repository GitHub View package NPM ## Example# * * * import {SearchField, Label, Input, Button} from 'react-aria-components'; <SearchField> <Label>Search</Label> <Input /> <Button>✕</Button> </SearchField> import { Button, Input, Label, SearchField } from 'react-aria-components'; <SearchField> <Label>Search</Label> <Input /> <Button>✕</Button> </SearchField> import { Button, Input, Label, SearchField } from 'react-aria-components'; <SearchField> <Label>Search</Label> <Input /> <Button>✕</Button> </SearchField> Search✕ Show CSS @import "@react-aria/example-theme"; .react-aria-SearchField { display: grid; grid-template-areas: "label label" "input button" "help help"; grid-template-columns: 1fr auto; align-items: center; width: fit-content; color: var(--text-color); .react-aria-Input { grid-area: input; width: 100%; padding: 0.286rem 1.714rem 0.286rem 0.286rem; margin: 0; border: 1px solid var(--border-color); border-radius: 6px; background: var(--field-background); font-size: 1.143rem; color: var(--field-text-color); outline: none; &::-webkit-search-cancel-button, &::-webkit-search-decoration { -webkit-appearance: none; } &::placeholder { color: var(--text-color-placeholder); opacity: 1; } &[data-focused] { outline: 2px solid var(--focus-ring-color); outline-offset: -1px; } } .react-aria-Button { grid-area: button; width: 1.143rem; height: 1.143rem; border-radius: 1.143rem; margin-left: -1.429rem; font-size: 0.857rem; line-height: 0.857rem; vertical-align: middle; text-align: center; background: var(--gray-500); color: var(--gray-50); border: none; padding: 0; &[data-pressed] { background: var(--gray-600); } } &[data-empty] button { display: none; } } @import "@react-aria/example-theme"; .react-aria-SearchField { display: grid; grid-template-areas: "label label" "input button" "help help"; grid-template-columns: 1fr auto; align-items: center; width: fit-content; color: var(--text-color); .react-aria-Input { grid-area: input; width: 100%; padding: 0.286rem 1.714rem 0.286rem 0.286rem; margin: 0; border: 1px solid var(--border-color); border-radius: 6px; background: var(--field-background); font-size: 1.143rem; color: var(--field-text-color); outline: none; &::-webkit-search-cancel-button, &::-webkit-search-decoration { -webkit-appearance: none; } &::placeholder { color: var(--text-color-placeholder); opacity: 1; } &[data-focused] { outline: 2px solid var(--focus-ring-color); outline-offset: -1px; } } .react-aria-Button { grid-area: button; width: 1.143rem; height: 1.143rem; border-radius: 1.143rem; margin-left: -1.429rem; font-size: 0.857rem; line-height: 0.857rem; vertical-align: middle; text-align: center; background: var(--gray-500); color: var(--gray-50); border: none; padding: 0; &[data-pressed] { background: var(--gray-600); } } &[data-empty] button { display: none; } } @import "@react-aria/example-theme"; .react-aria-SearchField { display: grid; grid-template-areas: "label label" "input button" "help help"; grid-template-columns: 1fr auto; align-items: center; width: fit-content; color: var(--text-color); .react-aria-Input { grid-area: input; width: 100%; padding: 0.286rem 1.714rem 0.286rem 0.286rem; margin: 0; border: 1px solid var(--border-color); border-radius: 6px; background: var(--field-background); font-size: 1.143rem; color: var(--field-text-color); outline: none; &::-webkit-search-cancel-button, &::-webkit-search-decoration { -webkit-appearance: none; } &::placeholder { color: var(--text-color-placeholder); opacity: 1; } &[data-focused] { outline: 2px solid var(--focus-ring-color); outline-offset: -1px; } } .react-aria-Button { grid-area: button; width: 1.143rem; height: 1.143rem; border-radius: 1.143rem; margin-left: -1.429rem; font-size: 0.857rem; line-height: 0.857rem; vertical-align: middle; text-align: center; background: var(--gray-500); color: var(--gray-50); border: none; padding: 0; &[data-pressed] { background: var(--gray-600); } } &[data-empty] button { display: none; } } ## Features# * * * Search fields can be built with `<input type="search">`, but these can be hard to style consistently cross browser. `SearchField` helps achieve accessible search fields that can be styled as needed. * **Clearable** – A custom clear button can be shown to allow the input to be easily reset. * **Accessible** – Uses a native `<input type="search">` element, with support for the Enter and Escape keys to submit and clear the field, respectively. Label, description, and error message elements are automatically associated with the field. * **Validation** – Support for native HTML constraint validation with customizable UI, custom validation functions, realtime validation, and server-side validation errors. ## Anatomy# * * * Search fields consist of an input element, a label, and an optional clear button. `SearchField` automatically manages the labeling and relationships between the elements, and handles keyboard events. Users can press the Escape key to clear the search field, or the Enter key to trigger the `onSubmit` event. `SearchField` also supports optional description and error message elements, which can be used to provide more context about the field, and any validation messages. These are linked with the input via the `aria-describedby` attribute. import {Button, FieldError, Input, Label, SearchField, Text} from 'react-aria-components'; <SearchField> <Label /> <Input /> <Button /> <Text slot="description" /> <FieldError /> </SearchField> import { Button, FieldError, Input, Label, SearchField, Text } from 'react-aria-components'; <SearchField> <Label /> <Input /> <Button /> <Text slot="description" /> <FieldError /> </SearchField> import { Button, FieldError, Input, Label, SearchField, Text } from 'react-aria-components'; <SearchField> <Label /> <Input /> <Button /> <Text slot="description" /> <FieldError /> </SearchField> If there is no visual label, an `aria-label` or `aria-labelledby` prop must be passed instead to identify the element to screen readers. ### Concepts# `SearchField` makes use of the following concepts: Forms Validating and submitting form data, and integrating with form libraries. ### Composed components# A `SearchField` uses the following components, which may also be used standalone or reused in other components. Label A label provides context for an input element. Input An input allows a user to enter a plain text value with a keyboard. Button A button allows a user to perform an action. ## Starter kits# * * * To help kick-start your project, we offer starter kits that include example implementations of all React Aria components with various styling solutions. All components are fully styled, including support for dark mode, high contrast mode, and all UI states. Each starter comes with a pre-configured Storybook that you can experiment with, or use as a starting point for your own component library. Vanilla CSS Download ZIP Preview Tailwind CSS Download ZIP Preview ## Reusable wrappers# * * * If you will use a SearchField in multiple places in your app, you can wrap all of the pieces into a reusable component. This way, the DOM structure, styling code, and other logic are defined in a single place and reused everywhere to ensure consistency. This example wraps `SearchField` and all of its children together into a single component which accepts a `label` prop, which is passed to the right place. It also shows how to use the `description` slot to render help text, and `FieldError` component to render validation errors. import type {SearchFieldProps, ValidationResult} from 'react-aria-components'; import {FieldError, Text} from 'react-aria-components'; interface MySearchFieldProps extends SearchFieldProps { label?: string; description?: string; errorMessage?: string | ((validation: ValidationResult) => string); placeholder?: string; } export function MySearchField( { label, description, errorMessage, placeholder, ...props }: MySearchFieldProps ) { return ( <SearchField {...props}> {label && <Label>{label}</Label>} <Input placeholder={placeholder} /> <Button>✕</Button> {description && <Text slot="description">{description}</Text>} <FieldError>{errorMessage}</FieldError> </SearchField> ); } <MySearchField label="Search" /> import type { SearchFieldProps, ValidationResult } from 'react-aria-components'; import {FieldError, Text} from 'react-aria-components'; interface MySearchFieldProps extends SearchFieldProps { label?: string; description?: string; errorMessage?: | string | ((validation: ValidationResult) => string); placeholder?: string; } export function MySearchField( { label, description, errorMessage, placeholder, ...props }: MySearchFieldProps ) { return ( <SearchField {...props}> {label && <Label>{label}</Label>} <Input placeholder={placeholder} /> <Button>✕</Button> {description && ( <Text slot="description">{description}</Text> )} <FieldError>{errorMessage}</FieldError> </SearchField> ); } <MySearchField label="Search" /> import type { SearchFieldProps, ValidationResult } from 'react-aria-components'; import { FieldError, Text } from 'react-aria-components'; interface MySearchFieldProps extends SearchFieldProps { label?: string; description?: string; errorMessage?: | string | (( validation: ValidationResult ) => string); placeholder?: string; } export function MySearchField( { label, description, errorMessage, placeholder, ...props }: MySearchFieldProps ) { return ( <SearchField {...props} > {label && ( <Label> {label} </Label> )} <Input placeholder={placeholder} /> <Button>✕</Button> {description && ( <Text slot="description"> {description} </Text> )} <FieldError> {errorMessage} </FieldError> </SearchField> ); } <MySearchField label="Search" /> Search✕ ## Value# * * * ### Default value# A SearchField's `value` is empty by default, but an initial, uncontrolled, value can be provided using the `defaultValue` prop. <MySearchField label="Search" defaultValue="Puppies" /> <MySearchField label="Search" defaultValue="Puppies" /> <MySearchField label="Search" defaultValue="Puppies" /> Search✕ ### Controlled value# The `value` prop can be used to make the value controlled. The `onChange` event is fired when the user edits the text, and receives the new value. function Example() { let [text, setText] = React.useState(''); return ( <> <MySearchField label="Search" onChange={setText} /> <p>Mirrored text: {text}</p> </> ); } function Example() { let [text, setText] = React.useState(''); return ( <> <MySearchField label="Search" onChange={setText} /> <p>Mirrored text: {text}</p> </> ); } function Example() { let [text, setText] = React.useState(''); return ( <> <MySearchField label="Search" onChange={setText} /> <p> Mirrored text: {' '} {text} </p> </> ); } Search✕ Mirrored text: ### HTML forms# SearchField supports the `name` prop for integration with HTML forms. In addition, attributes such as `type`, `pattern`, `inputMode`, and others are passed through to the underlying `<input>` element. <MySearchField label="Email" name="email" type="email" /> <MySearchField label="Email" name="email" type="email" /> <MySearchField label="Email" name="email" type="email" /> Email✕ ## Events# * * * The most commonly used handlers for events in SearchField are the: * `onChange` prop which is triggered whenever the value is edited by the user. * `onSubmit` prop which is triggered whenever the value is submitted by the user (e.g. by pressing Enter). * `onClear` prop which is triggered whenever the value is cleared by the user (e.g. by pressing clear button or Escape key). The example below uses `onChange`, `onSubmit`, and `onClear` to update two separate elements with the text entered into the SearchField. function Example() { let [currentText, setCurrentText] = React.useState(''); let [submittedText, setSubmittedText] = React.useState(''); return ( <div> <MySearchField onClear={() => setCurrentText('')} onChange={setCurrentText} onSubmit={setSubmittedText} label="Your text" value={currentText} /> <p>Mirrored text: {currentText}</p> <p>Submitted text: {submittedText}</p> </div> ); } function Example() { let [currentText, setCurrentText] = React.useState(''); let [submittedText, setSubmittedText] = React.useState( '' ); return ( <div> <MySearchField onClear={() => setCurrentText('')} onChange={setCurrentText} onSubmit={setSubmittedText} label="Your text" value={currentText} /> <p>Mirrored text: {currentText}</p> <p>Submitted text: {submittedText}</p> </div> ); } function Example() { let [ currentText, setCurrentText ] = React.useState(''); let [ submittedText, setSubmittedText ] = React.useState(''); return ( <div> <MySearchField onClear={() => setCurrentText( '' )} onChange={setCurrentText} onSubmit={setSubmittedText} label="Your text" value={currentText} /> <p> Mirrored text: {' '} {currentText} </p> <p> Submitted text: {' '} {submittedText} </p> </div> ); } Your text✕ Mirrored text: Submitted text: ## Validation# * * * SearchField supports HTML constraint validation props such as `isRequired`, `minLength`, and `pattern`, as well as custom validation functions, realtime validation, and server-side validation. It can also be integrated with other form libraries. See the Forms guide to learn more. To display validation errors, add a `<FieldError>` element as a child of the SearchField. This allows you to render error messages from all of the above sources with consistent custom styles. import {Form, FieldError} from 'react-aria-components'; <Form> <SearchField name="search" isRequired> <Label>Search</Label> <Input /> <Button>✕</Button> <FieldError /> </SearchField> <Button type="submit">Submit</Button> </Form> import {Form, FieldError} from 'react-aria-components'; <Form> <SearchField name="search" isRequired> <Label>Search</Label> <Input /> <Button>✕</Button> <FieldError /> </SearchField> <Button type="submit">Submit</Button> </Form> import { FieldError, Form } from 'react-aria-components'; <Form> <SearchField name="search" isRequired > <Label> Search </Label> <Input /> <Button>✕</Button> <FieldError /> </SearchField> <Button type="submit"> Submit </Button> </Form> Search✕ Submit Show CSS .react-aria-SearchField { .react-aria-Input{ &[data-invalid] { border-color: var(--invalid-color); } } .react-aria-FieldError { grid-area: help; font-size: 12px; color: var(--invalid-color); } } .react-aria-SearchField { .react-aria-Input{ &[data-invalid] { border-color: var(--invalid-color); } } .react-aria-FieldError { grid-area: help; font-size: 12px; color: var(--invalid-color); } } .react-aria-SearchField { .react-aria-Input{ &[data-invalid] { border-color: var(--invalid-color); } } .react-aria-FieldError { grid-area: help; font-size: 12px; color: var(--invalid-color); } } By default, `FieldError` displays default validation messages provided by the browser. See Customizing error messages in the Forms guide to learn how to provide your own custom errors. ### Description# The `description` slot can be used to associate additional help text with a search field. <SearchField> <Label>Email</Label> <Input /> <Button>✕</Button> <Text slot="description"> Enter an email for us to contact you about your order. </Text></SearchField> <SearchField> <Label>Email</Label> <Input /> <Button>✕</Button> <Text slot="description"> Enter an email for us to contact you about your order. </Text></SearchField> <SearchField> <Label>Email</Label> <Input /> <Button>✕</Button> <Text slot="description"> Enter an email for us to contact you about your order. </Text></SearchField> Email✕Enter an email for us to contact you about your order. Show CSS .react-aria-SearchField { [slot=description] { grid-area: help; font-size: 12px; } } .react-aria-SearchField { [slot=description] { grid-area: help; font-size: 12px; } } .react-aria-SearchField { [slot=description] { grid-area: help; font-size: 12px; } } ## Disabled# * * * A SearchField can be disabled using the `isDisabled` prop. <MySearchField label="Email" isDisabled /> <MySearchField label="Email" isDisabled /> <MySearchField label="Email" isDisabled /> Email✕ Show CSS .react-aria-SearchField { .react-aria-Input { &[data-disabled] { border-color: var(--border-color-disabled); color: var(--text-color-disabled); } } } .react-aria-SearchField { .react-aria-Input { &[data-disabled] { border-color: var(--border-color-disabled); color: var(--text-color-disabled); } } } .react-aria-SearchField { .react-aria-Input { &[data-disabled] { border-color: var(--border-color-disabled); color: var(--text-color-disabled); } } } ### Read only# The `isReadOnly` boolean prop makes the SearchField's text content immutable. Unlike `isDisabled`, the SearchField remains focusable and the contents can still be copied. See the MDN docs for more information. <MySearchField label="Email" defaultValue="abc@adobe.com" isReadOnly /> <MySearchField label="Email" defaultValue="abc@adobe.com" isReadOnly /> <MySearchField label="Email" defaultValue="abc@adobe.com" isReadOnly /> Email✕ ## Props# * * * ### SearchField# | Name | Type | Default | Description | | --- | --- | --- | --- | | `enterKeyHint` | `'enter' | 'done' | 'go' | 'next' | 'previous' | 'search' | 'send'` | — | An enumerated attribute that defines what action label or icon to preset for the enter key on virtual keyboards. See MDN. | | `isDisabled` | `boolean` | — | Whether the input is disabled. | | `isReadOnly` | `boolean` | — | Whether the input can be selected but not changed by the user. | | `isRequired` | `boolean` | — | Whether user input is required on the input before form submission. | | `isInvalid` | `boolean` | — | Whether the input value is invalid. | | `validate` | `( (value: string )) => ValidationError | true | null | undefined` | — | A function that returns an error message if a given value is invalid. Validation errors are displayed to the user when the form is submitted if `validationBehavior="native"`. For realtime validation, use the `isInvalid` prop instead. | | `autoFocus` | `boolean` | — | Whether the element should receive focus on render. | | `value` | `string` | — | The current value (controlled). | | `defaultValue` | `string` | — | The default value (uncontrolled). | | `autoComplete` | `string` | — | Describes the type of autocomplete functionality the input should provide if any. See MDN. | | `maxLength` | `number` | — | The maximum number of characters supported by the input. See MDN. | | `minLength` | `number` | — | The minimum number of characters required by the input. See MDN. | | `pattern` | `string` | — | Regex pattern that the value of the input must match to be valid. See MDN. | | `type` | `'text' | 'search' | 'url' | 'tel' | 'email' | 'password' | string & & {}` | — | The type of input to render. See MDN. | | `inputMode` | `'none' | 'text' | 'tel' | 'url' | 'email' | 'numeric' | 'decimal' | 'search'` | — | Hints at the type of data that might be entered by the user while editing the element or its contents. See MDN. | | `autoCorrect` | `string` | — | An attribute that takes as its value a space-separated string that describes what, if any, type of autocomplete functionality the input should provide. See MDN. | | `spellCheck` | `string` | — | An enumerated attribute that defines whether the element may be checked for spelling errors. See MDN. | | `name` | `string` | — | The name of the input element, used when submitting an HTML form. See MDN. | | `validationBehavior` | `'native' | 'aria'` | `'native'` | Whether to use native HTML form validation to prevent form submission when the value is missing or invalid, or mark the field as required or invalid via ARIA. | | `children` | `ReactNode | ( (values: SearchFieldRenderProps & & { defaultChildren: ReactNode | | undefined } )) => ReactNode` | — | The children of the component. A function may be provided to alter the children based on component state. | | `className` | `string | ( (values: SearchFieldRenderProps & & { defaultClassName: string | | undefined } )) => string` | — | The CSS className for the element. A function may be provided to compute the class based on component state. | | `style` | `CSSProperties | ( (values: SearchFieldRenderProps & & { defaultStyle: CSSProperties } )) => CSSProperties | undefined` | — | The inline style for the element. A function may be provided to compute the style based on component state. | Events | Name | Type | Description | | --- | --- | --- | | `onSubmit` | `( (value: string )) => void` | Handler that is called when the SearchField is submitted. | | `onClear` | `() => void` | Handler that is called when the clear button is pressed. | | `onFocus` | `( (e: FocusEvent<T> )) => void` | Handler that is called when the element receives focus. | | `onBlur` | `( (e: FocusEvent<T> )) => void` | Handler that is called when the element loses focus. | | `onFocusChange` | `( (isFocused: boolean )) => void` | Handler that is called when the element's focus status changes. | | `onKeyDown` | `( (e: KeyboardEvent )) => void` | Handler that is called when a key is pressed. | | `onKeyUp` | `( (e: KeyboardEvent )) => void` | Handler that is called when a key is released. | | `onChange` | `( (value: T )) => void` | Handler that is called when the value changes. | | `onCopy` | `ClipboardEventHandler<HTMLInputElement>` | Handler that is called when the user copies text. See MDN. | | `onCut` | `ClipboardEventHandler<HTMLInputElement>` | Handler that is called when the user cuts text. See MDN. | | `onPaste` | `ClipboardEventHandler<HTMLInputElement>` | Handler that is called when the user pastes text. See MDN. | | `onCompositionStart` | `CompositionEventHandler<HTMLInputElement>` | Handler that is called when a text composition system starts a new text composition session. See MDN. | | `onCompositionEnd` | `CompositionEventHandler<HTMLInputElement>` | Handler that is called when a text composition system completes or cancels the current text composition session. See MDN. | | `onCompositionUpdate` | `CompositionEventHandler<HTMLInputElement>` | Handler that is called when a new character is received in the current text composition session. See MDN. | | `onSelect` | `ReactEventHandler<HTMLInputElement>` | Handler that is called when text in the input is selected. See MDN. | | `onBeforeInput` | `FormEventHandler<HTMLInputElement>` | Handler that is called when the input value is about to be modified. See MDN. | | `onInput` | `FormEventHandler<HTMLInputElement>` | Handler that is called when the input value is modified. See MDN. | Layout | Name | Type | Description | | --- | --- | --- | | `slot` | `string | null` | A slot name for the component. Slots allow the component to receive props from a parent component. An explicit `null` value indicates that the local props completely override all props received from a parent. | Accessibility | Name | Type | Description | | --- | --- | --- | | `id` | `string` | The element's unique identifier. See MDN. | | `excludeFromTabOrder` | `boolean` | Whether to exclude the element from the sequential tab order. If true, the element will not be focusable via the keyboard by tabbing. This should be avoided except in rare scenarios where an alternative means of accessing the element or its functionality via the keyboard is available. | | `aria-activedescendant` | `string` | Identifies the currently active element when DOM focus is on a composite widget, textbox, group, or application. | | `aria-autocomplete` | `'none' | 'inline' | 'list' | 'both'` | Indicates whether inputting text could trigger display of one or more predictions of the user's intended value for an input and specifies how predictions would be presented if they are made. | | `aria-haspopup` | `boolean | 'false' | 'true' | 'menu' | 'listbox' | 'tree' | 'grid' | 'dialog'` | Indicates the availability and type of interactive popup element, such as menu or dialog, that can be triggered by an element. | | `aria-controls` | `string` | Identifies the element (or elements) whose contents or presence are controlled by the current element. | | `aria-label` | `string` | Defines a string value that labels the current element. | | `aria-labelledby` | `string` | Identifies the element (or elements) that labels the current element. | | `aria-describedby` | `string` | Identifies the element (or elements) that describes the object. | | `aria-details` | `string` | Identifies the element (or elements) that provide a detailed, extended description for the object. | | `aria-errormessage` | `string` | Identifies the element that provides an error message for the object. | ### Label# A `<Label>` accepts all HTML attributes. ### Input# An `<Input>` accepts all HTML attributes. ### Button# A `<Button>` accepts its contents as `children`. Other props such as `onPress` and `isDisabled` will be set by the `SearchField`. Show props | Name | Type | Default | Description | | --- | --- | --- | --- | | `form` | `string` | — | The `<form>` element to associate the button with. The value of this attribute must be the id of a `<form>` in the same document. | | `formAction` | `string` | — | The URL that processes the information submitted by the button. Overrides the action attribute of the button's form owner. | | `formEncType` | `string` | — | Indicates how to encode the form data that is submitted. | | `formMethod` | `string` | — | Indicates the HTTP method used to submit the form. | | `formNoValidate` | `boolean` | — | Indicates that the form is not to be validated when it is submitted. | | `formTarget` | `string` | — | Overrides the target attribute of the button's form owner. | | `name` | `string` | — | Submitted as a pair with the button's value as part of the form data. | | `value` | `string` | — | The value associated with the button's name when it's submitted with the form data. | | `isPending` | `boolean` | — | Whether the button is in a pending state. This disables press and hover events while retaining focusability, and announces the pending state to screen readers. | | `isDisabled` | `boolean` | — | Whether the button is disabled. | | `autoFocus` | `boolean` | — | Whether the element should receive focus on render. | | `type` | `'button' | 'submit' | 'reset'` | `'button'` | The behavior of the button when used in an HTML form. | | `children` | `ReactNode | ( (values: ButtonRenderProps & & { defaultChildren: ReactNode | | undefined } )) => ReactNode` | — | The children of the component. A function may be provided to alter the children based on component state. | | `className` | `string | ( (values: ButtonRenderProps & & { defaultClassName: string | | undefined } )) => string` | — | The CSS className for the element. A function may be provided to compute the class based on component state. | | `style` | `CSSProperties | ( (values: ButtonRenderProps & & { defaultStyle: CSSProperties } )) => CSSProperties | undefined` | — | The inline style for the element. A function may be provided to compute the style based on component state. | Events | Name | Type | Description | | --- | --- | --- | | `onPress` | `( (e: PressEvent )) => void` | Handler that is called when the press is released over the target. | | `onPressStart` | `( (e: PressEvent )) => void` | Handler that is called when a press interaction starts. | | `onPressEnd` | `( (e: PressEvent )) => void` | Handler that is called when a press interaction ends, either over the target or when the pointer leaves the target. | | `onPressChange` | `( (isPressed: boolean )) => void` | Handler that is called when the press state changes. | | `onPressUp` | `( (e: PressEvent )) => void` | Handler that is called when a press is released over the target, regardless of whether it started on the target or not. | | `onClick` | `( (e: MouseEvent<FocusableElement> )) => void` | **Not recommended – use `onPress` instead.** `onClick` is an alias for `onPress` provided for compatibility with other libraries. `onPress` provides additional event details for non-mouse interactions. | | `onFocus` | `( (e: FocusEvent<Target> )) => void` | Handler that is called when the element receives focus. | | `onBlur` | `( (e: FocusEvent<Target> )) => void` | Handler that is called when the element loses focus. | | `onFocusChange` | `( (isFocused: boolean )) => void` | Handler that is called when the element's focus status changes. | | `onKeyDown` | `( (e: KeyboardEvent )) => void` | Handler that is called when a key is pressed. | | `onKeyUp` | `( (e: KeyboardEvent )) => void` | Handler that is called when a key is released. | | `onHoverStart` | `( (e: HoverEvent )) => void` | Handler that is called when a hover interaction starts. | | `onHoverEnd` | `( (e: HoverEvent )) => void` | Handler that is called when a hover interaction ends. | | `onHoverChange` | `( (isHovering: boolean )) => void` | Handler that is called when the hover state changes. | Layout | Name | Type | Description | | --- | --- | --- | | `slot` | `string | null` | A slot name for the component. Slots allow the component to receive props from a parent component. An explicit `null` value indicates that the local props completely override all props received from a parent. | Accessibility | Name | Type | Description | | --- | --- | --- | | `id` | `string` | The element's unique identifier. See MDN. | | `excludeFromTabOrder` | `boolean` | Whether to exclude the element from the sequential tab order. If true, the element will not be focusable via the keyboard by tabbing. This should be avoided except in rare scenarios where an alternative means of accessing the element or its functionality via the keyboard is available. | | `preventFocusOnPress` | `boolean` | Whether to prevent focus from moving to the button when pressing it. Caution, this can make the button inaccessible and should only be used when alternative keyboard interaction is provided, such as ComboBox's MenuTrigger or a NumberField's increment/decrement control. | | `aria-expanded` | `boolean | 'true' | 'false'` | Indicates whether the element, or another grouping element it controls, is currently expanded or collapsed. | | `aria-haspopup` | `boolean | 'menu' | 'listbox' | 'tree' | 'grid' | 'dialog' | 'true' | 'false'` | Indicates the availability and type of interactive popup element, such as menu or dialog, that can be triggered by an element. | | `aria-controls` | `string` | Identifies the element (or elements) whose contents or presence are controlled by the current element. | | `aria-pressed` | `boolean | 'true' | 'false' | 'mixed'` | Indicates the current "pressed" state of toggle buttons. | | `aria-current` | `boolean | 'true' | 'false' | 'page' | 'step' | 'location' | 'date' | 'time'` | Indicates whether this element represents the current item within a container or set of related elements. | | `aria-label` | `string` | Defines a string value that labels the current element. | | `aria-labelledby` | `string` | Identifies the element (or elements) that labels the current element. | | `aria-describedby` | `string` | Identifies the element (or elements) that describes the object. | | `aria-details` | `string` | Identifies the element (or elements) that provide a detailed, extended description for the object. | ### Text# `<Text>` accepts all HTML attributes. ### FieldError# A `<FieldError>` displays validation errors. Show props | Name | Type | Description | | --- | --- | --- | | `children` | `ReactNode | ( (values: FieldErrorRenderProps & & { defaultChildren: ReactNode | | undefined } )) => ReactNode` | The children of the component. A function may be provided to alter the children based on component state. | | `className` | `string | ( (values: FieldErrorRenderProps & & { defaultClassName: string | | undefined } )) => string` | The CSS className for the element. A function may be provided to compute the class based on component state. | | `style` | `CSSProperties | ( (values: FieldErrorRenderProps & & { defaultStyle: CSSProperties } )) => CSSProperties | undefined` | The inline style for the element. A function may be provided to compute the style based on component state. | Accessibility | Name | Type | Description | | --- | --- | --- | | `id` | `string` | The element's unique identifier. See MDN. | ## Styling# * * * React Aria components can be styled in many ways, including using CSS classes, inline styles, utility classes (e.g. Tailwind), CSS-in-JS (e.g. Styled Components), etc. By default, all components include a builtin `className` attribute which can be targeted using CSS selectors. These follow the `react-aria-ComponentName` naming convention. .react-aria-SearchField { /* ... */ } .react-aria-SearchField { /* ... */ } .react-aria-SearchField { /* ... */ } A custom `className` can also be specified on any component. This overrides the default `className` provided by React Aria with your own. <SearchField className="my-searchfield"> {/* ... */} </SearchField> <SearchField className="my-searchfield"> {/* ... */} </SearchField> <SearchField className="my-searchfield"> {/* ... */} </SearchField> In addition, some components support multiple UI states (e.g. focused, placeholder, readonly, etc.). React Aria components expose states using data attributes, which you can target in CSS selectors. For example: input[data-hovered] { /* ... */ } input[data-disabled] { /* ... */ } input[data-hovered] { /* ... */ } input[data-disabled] { /* ... */ } input[data-hovered] { /* ... */ } input[data-disabled] { /* ... */ } The `className` and `style` props also accept functions which receive states for styling. This lets you dynamically determine the classes or styles to apply, which is useful when using utility CSS libraries like Tailwind. <Button className={({ isPressed }) => isPressed ? 'bg-gray-700' : 'bg-gray-600'} /> <Button className={({ isPressed }) => isPressed ? 'bg-gray-700' : 'bg-gray-600'} /> <Button className={( { isPressed } ) => isPressed ? 'bg-gray-700' : 'bg-gray-600'} /> Render props may also be used as children to alter what elements are rendered based on the current state. For example, you could render the clear button only when the input is non-empty. <SearchField> {({state}) => ( <> <Label>Search</Label> <Input /> {state.value !== '' && <Button>✕</Button>} </> )} </SearchField> <SearchField> {({state}) => ( <> <Label>Search</Label> <Input /> {state.value !== '' && <Button>✕</Button>} </> )} </SearchField> <SearchField> {({ state }) => ( <> <Label> Search </Label> <Input /> {state.value !== '' && ( <Button> ✕ </Button> )} </> )} </SearchField> The states, selectors, and render props for each component used in a `SearchField` are documented below. ### SearchField# A `SearchField` can be targeted with the `.react-aria-SearchField` CSS selector, or by overriding with a custom `className`. It supports the following states: | Name | CSS Selector | Description | | --- | --- | --- | | `isEmpty` | `[data-empty]` | Whether the search field is empty. | | `isDisabled` | `[data-disabled]` | Whether the search field is disabled. | | `isInvalid` | `[data-invalid]` | Whether the search field is invalid. | | `state` | `—` | State of the search field. | ### Label# A `Label` can be targeted with the `.react-aria-Label` CSS selector, or by overriding with a custom `className`. ### Input# An `Input` can be targeted with the `.react-aria-Input` CSS selector, or by overriding with a custom `className`. It supports the following states: | Name | CSS Selector | Description | | --- | --- | --- | | `isHovered` | `[data-hovered]` | Whether the input is currently hovered with a mouse. | | `isFocused` | `[data-focused]` | Whether the input is focused, either via a mouse or keyboard. | | `isFocusVisible` | `[data-focus-visible]` | Whether the input is keyboard focused. | | `isDisabled` | `[data-disabled]` | Whether the input is disabled. | | `isInvalid` | `[data-invalid]` | Whether the input is invalid. | ### Button# A Button can be targeted with the `.react-aria-Button` CSS selector, or by overriding with a custom `className`. It supports the following states: | Name | CSS Selector | Description | | --- | --- | --- | | `isHovered` | `[data-hovered]` | Whether the button is currently hovered with a mouse. | | `isPressed` | `[data-pressed]` | Whether the button is currently in a pressed state. | | `isFocused` | `[data-focused]` | Whether the button is focused, either via a mouse or keyboard. | | `isFocusVisible` | `[data-focus-visible]` | Whether the button is keyboard focused. | | `isDisabled` | `[data-disabled]` | Whether the button is disabled. | | `isPending` | `[data-pending]` | Whether the button is currently in a pending state. | ### Text# The help text elements within a `SearchField` can be targeted with the `[slot=description]` and `[slot=errorMessage]` CSS selectors, or by adding a custom `className`. ### FieldError# A `FieldError` can be targeted with the `.react-aria-FieldError` CSS selector, or by overriding with a custom `className`. It supports the following render props: | Name | Description | | --- | --- | | `isInvalid` | Whether the input value is invalid. | | `validationErrors` | The current error messages for the input if it is invalid, otherwise an empty array. | | `validationDetails` | The native validation details for the input. | ## Advanced customization# * * * ### Composition# If you need to customize one of the components within a `SearchField`, such as `Label` or `Input`, in many cases you can create a wrapper component. This lets you customize the props passed to the component. function MyInput(props) { return <Input {...props} className="my-input" /> } function MyInput(props) { return <Input {...props} className="my-input" /> } function MyInput(props) { return ( <Input {...props} className="my-input" /> ); } ### Contexts# All React Aria Components export a corresponding context that can be used to send props to them from a parent element. This enables you to build your own compositional APIs similar to those found in React Aria Components itself. You can send any prop or ref via context that you could pass to the corresponding component. The local props and ref on the component are merged with the ones passed via context, with the local props taking precedence (following the rules documented in mergeProps). | Component | Context | Props | Ref | | --- | --- | --- | --- | | `SearchField` | `SearchFieldContext` | ` SearchFieldProps ` | `HTMLDivElement` | This example shows a `FieldGroup` component that renders a group of search fields with a title. The entire group can be marked as disabled via the `isDisabled` prop, which is passed to all child search fields via the `SearchFieldContext` provider. import {SearchFieldContext} from 'react-aria-components'; interface FieldGroupProps { title?: string, children?: React.ReactNode, isDisabled?: boolean } function FieldGroup({title, children, isDisabled}: FieldGroupProps) { return ( <fieldset> <legend>{title}</legend> <SearchFieldContext.Provider value={{isDisabled}}> {children} </SearchFieldContext.Provider> </fieldset> ); } <FieldGroup title="Filters" isDisabled> <MySearchField label="Name" defaultValue="Devon" /> <MySearchField label="Email" defaultValue="devon@example.com" /> </FieldGroup> import {SearchFieldContext} from 'react-aria-components'; interface FieldGroupProps { title?: string; children?: React.ReactNode; isDisabled?: boolean; } function FieldGroup( { title, children, isDisabled }: FieldGroupProps ) { return ( <fieldset> <legend>{title}</legend> <SearchFieldContext.Provider value={{ isDisabled }}> {children} </SearchFieldContext.Provider> </fieldset> ); } <FieldGroup title="Filters" isDisabled> <MySearchField label="Name" defaultValue="Devon" /> <MySearchField label="Email" defaultValue="devon@example.com" /> </FieldGroup> import {SearchFieldContext} from 'react-aria-components'; interface FieldGroupProps { title?: string; children?: React.ReactNode; isDisabled?: boolean; } function FieldGroup( { title, children, isDisabled }: FieldGroupProps ) { return ( <fieldset> <legend> {title} </legend> <SearchFieldContext.Provider value={{ isDisabled }} > {children} </SearchFieldContext.Provider> </fieldset> ); } <FieldGroup title="Filters" isDisabled > <MySearchField label="Name" defaultValue="Devon" /> <MySearchField label="Email" defaultValue="devon@example.com" /> </FieldGroup> Filters Name✕ Email✕ Show CSS fieldset { padding: 1.5em; width: fit-content; } fieldset { padding: 1.5em; width: fit-content; } fieldset { padding: 1.5em; width: fit-content; } ### Custom children# SearchField passes props to its child components, such as the label and input, via their associated contexts. These contexts are exported so you can also consume them in your own custom components. This enables you to reuse existing components from your app or component library together with React Aria Components. | Component | Context | Props | Ref | | --- | --- | --- | --- | | `Label` | `LabelContext` | ` LabelProps ` | `HTMLLabelElement` | | `Input` | `InputContext` | ` InputProps ` | `HTMLInputElement` | | `Button` | `ButtonContext` | ` ButtonProps ` | `HTMLButtonElement` | | `Text` | `TextContext` | ` TextProps ` | `HTMLElement` | This example consumes from `LabelContext` in an existing styled label component to make it compatible with React Aria Components. The `useContextProps` hook merges the local props and ref with the ones provided via context by SearchField. import type {LabelProps} from 'react-aria-components'; import {LabelContext, useContextProps} from 'react-aria-components'; const MyCustomLabel = React.forwardRef( (props: LabelProps, ref: React.ForwardedRef<HTMLLabelElement>) => { // Merge the local props and ref with the ones provided via context. [props, ref] = useContextProps(props, ref, LabelContext); // ... your existing Label component return <label {...props} ref={ref} />; } ); import type {LabelProps} from 'react-aria-components'; import { LabelContext, useContextProps } from 'react-aria-components'; const MyCustomLabel = React.forwardRef( ( props: LabelProps, ref: React.ForwardedRef<HTMLLabelElement> ) => { // Merge the local props and ref with the ones provided via context. [props, ref] = useContextProps( props, ref, LabelContext ); // ... your existing Label component return <label {...props} ref={ref} />; } ); import type {LabelProps} from 'react-aria-components'; import { LabelContext, useContextProps } from 'react-aria-components'; const MyCustomLabel = React.forwardRef( ( props: LabelProps, ref: React.ForwardedRef< HTMLLabelElement > ) => { // Merge the local props and ref with the ones provided via context. [props, ref] = useContextProps( props, ref, LabelContext ); // ... your existing Label component return ( <label {...props} ref={ref} /> ); } ); Now you can use `MyCustomLabel` within a `SearchField`, in place of the builtin React Aria Components `Label`. <SearchField> <MyCustomLabel>Name</MyCustomLabel> <Input /> </SearchField> <SearchField> <MyCustomLabel>Name</MyCustomLabel> <Input /> </SearchField> <SearchField> <MyCustomLabel> Name </MyCustomLabel> <Input /> </SearchField> ### Hooks# If you need to customize things even further, such as accessing internal state or customizing DOM structure, you can drop down to the lower level Hook-based API. See useSearchField for more details. --- ## Page: https://react-spectrum.adobe.com/react-aria/Slider.html # Slider A slider allows a user to select one or more values within a range. <table><tbody><tr><th>install</th><td><code>yarn add react-aria-components</code></td></tr><tr><th>version</th><td>1.10.1</td></tr><tr><th>usage</th><td><code><span>import</span> {Slider} <span>from</span> <span>'react-aria-components'</span></code></td></tr></tbody></table> View ARIA pattern W3C View repository GitHub View package NPM ## Example# * * * import {Label, Slider, SliderOutput, SliderThumb, SliderTrack} from 'react-aria-components'; <Slider defaultValue={30}> <Label>Opacity</Label> <SliderOutput /> <SliderTrack> <SliderThumb /> </SliderTrack> </Slider> import { Label, Slider, SliderOutput, SliderThumb, SliderTrack } from 'react-aria-components'; <Slider defaultValue={30}> <Label>Opacity</Label> <SliderOutput /> <SliderTrack> <SliderThumb /> </SliderTrack> </Slider> import { Label, Slider, SliderOutput, SliderThumb, SliderTrack } from 'react-aria-components'; <Slider defaultValue={30} > <Label> Opacity </Label> <SliderOutput /> <SliderTrack> <SliderThumb /> </SliderTrack> </Slider> Opacity 30 Show CSS @import "@react-aria/example-theme"; .react-aria-Slider { display: grid; grid-template-areas: "label output" "track track"; grid-template-columns: 1fr auto; max-width: 300px; color: var(--text-color); .react-aria-Label { grid-area: label; } .react-aria-SliderOutput { grid-area: output; } .react-aria-SliderTrack { grid-area: track; position: relative; /* track line */ &:before { content: ''; display: block; position: absolute; background: var(--border-color); } } .react-aria-SliderThumb { width: 1.429rem; height: 1.429rem; border-radius: 50%; background: var(--highlight-background); border: 2px solid var(--background-color); forced-color-adjust: none; &[data-dragging] { background: var(--highlight-background-pressed); } &[data-focus-visible] { outline: 2px solid var(--focus-ring-color); } } &[data-orientation=horizontal] { flex-direction: column; width: 300px; .react-aria-SliderTrack { height: 30px; width: 100%; &:before { height: 3px; width: 100%; top: 50%; transform: translateY(-50%); } } .react-aria-SliderThumb { top: 50%; } } } @import "@react-aria/example-theme"; .react-aria-Slider { display: grid; grid-template-areas: "label output" "track track"; grid-template-columns: 1fr auto; max-width: 300px; color: var(--text-color); .react-aria-Label { grid-area: label; } .react-aria-SliderOutput { grid-area: output; } .react-aria-SliderTrack { grid-area: track; position: relative; /* track line */ &:before { content: ''; display: block; position: absolute; background: var(--border-color); } } .react-aria-SliderThumb { width: 1.429rem; height: 1.429rem; border-radius: 50%; background: var(--highlight-background); border: 2px solid var(--background-color); forced-color-adjust: none; &[data-dragging] { background: var(--highlight-background-pressed); } &[data-focus-visible] { outline: 2px solid var(--focus-ring-color); } } &[data-orientation=horizontal] { flex-direction: column; width: 300px; .react-aria-SliderTrack { height: 30px; width: 100%; &:before { height: 3px; width: 100%; top: 50%; transform: translateY(-50%); } } .react-aria-SliderThumb { top: 50%; } } } @import "@react-aria/example-theme"; .react-aria-Slider { display: grid; grid-template-areas: "label output" "track track"; grid-template-columns: 1fr auto; max-width: 300px; color: var(--text-color); .react-aria-Label { grid-area: label; } .react-aria-SliderOutput { grid-area: output; } .react-aria-SliderTrack { grid-area: track; position: relative; /* track line */ &:before { content: ''; display: block; position: absolute; background: var(--border-color); } } .react-aria-SliderThumb { width: 1.429rem; height: 1.429rem; border-radius: 50%; background: var(--highlight-background); border: 2px solid var(--background-color); forced-color-adjust: none; &[data-dragging] { background: var(--highlight-background-pressed); } &[data-focus-visible] { outline: 2px solid var(--focus-ring-color); } } &[data-orientation=horizontal] { flex-direction: column; width: 300px; .react-aria-SliderTrack { height: 30px; width: 100%; &:before { height: 3px; width: 100%; top: 50%; transform: translateY(-50%); } } .react-aria-SliderThumb { top: 50%; } } } ## Features# * * * The <input type="range"> HTML element can be used to build a slider, however it is very difficult to style cross browser. `Slider` helps achieve accessible sliders that can be styled as needed. * **Customizable** – Support for one or multiple thumbs, in both horizontal and vertical orientations. The whole slider, or individual thumbs can be disabled. Custom minimum, maximum, and step values are supported as well. * **High quality interactions** – Mouse, touch, and keyboard input is supported via the useMove hook. Pressing the track moves the nearest thumb to that position. Text selection and touch scrolling are prevented while dragging. * **Touch friendly** – Multiple thumbs or sliders can be dragged at once on multi-touch screens. * **Accessible** – Slider thumbs use visually hidden `<input>` elements for mobile screen reader support, and HTML form integration. `<label>` and `<output>` elements are automatically associated to provide context for assistive technologies. * **International** – Output value is formatted as a percentage or custom format according to the user's locale. The slider automatically mirrors all interactions in right-to-left languages. ## Anatomy# * * * Sliders consist of a track element showing the range of available values, one or more thumbs showing the current values, an optional <output> element displaying the current values textually, and a label. The thumbs can be dragged to allow a user to change their value. In addition, the track can be clicked to move the nearest thumb to that position. import {Label, Slider, SliderOutput, SliderThumb, SliderTrack} from 'react-aria-components'; <Slider> <Label /> <SliderOutput /> <SliderTrack> <SliderThumb /> <SliderThumb> <Label /> </SliderThumb> </SliderTrack> </Slider> import { Label, Slider, SliderOutput, SliderThumb, SliderTrack } from 'react-aria-components'; <Slider> <Label /> <SliderOutput /> <SliderTrack> <SliderThumb /> <SliderThumb> <Label /> </SliderThumb> </SliderTrack> </Slider> import { Label, Slider, SliderOutput, SliderThumb, SliderTrack } from 'react-aria-components'; <Slider> <Label /> <SliderOutput /> <SliderTrack> <SliderThumb /> <SliderThumb> <Label /> </SliderThumb> </SliderTrack> </Slider> If there is no visual label, an `aria-label` or `aria-labelledby` prop must be passed instead to identify the slider and thumbs to screen readers. ### Composed components# A `Slider` uses the following components, which may also be used standalone or reused in other components. Label A label provides context for an input element. ## Examples# * * * Opacity Slider An opacity slider styled with Tailwind CSS. ## Starter kits# * * * To help kick-start your project, we offer starter kits that include example implementations of all React Aria components with various styling solutions. All components are fully styled, including support for dark mode, high contrast mode, and all UI states. Each starter comes with a pre-configured Storybook that you can experiment with, or use as a starting point for your own component library. Vanilla CSS Download ZIP Preview Tailwind CSS Download ZIP Preview ## Reusable wrappers# * * * If you will use a Slider in multiple places in your app, you can wrap all of the pieces into a reusable component. This way, the DOM structure, styling code, and other logic are defined in a single place and reused everywhere to ensure consistency. This example wraps `Slider` and all of its children together into a single component which accepts a `label` prop, which is passed to the right place. It also shows how to use the `SliderTrack` and `SliderOutput` render props to render multiple slider thumbs, depending on the provided values. When multiple thumbs are rendered, each `SliderThumb` should have an `aria-label`, which is provided via the `thumbLabels` prop in this example. import type {SliderProps} from 'react-aria-components'; interface MySliderProps<T> extends SliderProps<T> { label?: string; thumbLabels?: string[]; } function MySlider<T extends number | number[]>( { label, thumbLabels, ...props }: MySliderProps<T> ) { return ( <Slider {...props}> {label && <Label>{label}</Label>} <SliderOutput> {({ state }) => state.values.map((_, i) => state.getThumbValueLabel(i)).join(' – ')} </SliderOutput> <SliderTrack> {({ state }) => state.values.map((_, i) => ( <SliderThumb key={i} index={i} aria-label={thumbLabels?.[i]} /> ))} </SliderTrack> </Slider> ); } <MySlider label="Range" defaultValue={[30, 60]} thumbLabels={['start', 'end']} /> import type {SliderProps} from 'react-aria-components'; interface MySliderProps<T> extends SliderProps<T> { label?: string; thumbLabels?: string[]; } function MySlider<T extends number | number[]>( { label, thumbLabels, ...props }: MySliderProps<T> ) { return ( <Slider {...props}> {label && <Label>{label}</Label>} <SliderOutput> {({ state }) => state.values.map((_, i) => state.getThumbValueLabel(i) ).join(' – ')} </SliderOutput> <SliderTrack> {({ state }) => state.values.map((_, i) => ( <SliderThumb key={i} index={i} aria-label={thumbLabels?.[i]} /> ))} </SliderTrack> </Slider> ); } <MySlider label="Range" defaultValue={[30, 60]} thumbLabels={['start', 'end']} /> import type {SliderProps} from 'react-aria-components'; interface MySliderProps< T > extends SliderProps<T> { label?: string; thumbLabels?: string[]; } function MySlider< T extends | number | number[] >( { label, thumbLabels, ...props }: MySliderProps<T> ) { return ( <Slider {...props}> {label && ( <Label> {label} </Label> )} <SliderOutput> {({ state }) => state.values .map(( _, i ) => state .getThumbValueLabel( i ) ).join( ' – ' )} </SliderOutput> <SliderTrack> {({ state }) => state.values .map(( _, i ) => ( <SliderThumb key={i} index={i} aria-label={thumbLabels ?.[i]} /> ))} </SliderTrack> </Slider> ); } <MySlider label="Range" defaultValue={[ 30, 60 ]} thumbLabels={[ 'start', 'end' ]} /> Range 30 – 60 ## Value# * * * ### Controlled value# The `value` prop paired with the `onChange` event can be used to make a slider controlled. The value must fall between the Slider's minimum and maximum values, which default to 0 and 100 respectively. The `onChange` event receives the new slider value as a parameter, which can be used to update state. function Example() { let [value, setValue] = React.useState(25); return ( <> <MySlider<number> label="Cookies to buy" value={value} onChange={setValue} /> <p>Current value: {value}</p> </> ); } function Example() { let [value, setValue] = React.useState(25); return ( <> <MySlider<number> label="Cookies to buy" value={value} onChange={setValue} /> <p>Current value: {value}</p> </> ); } function Example() { let [value, setValue] = React.useState(25); return ( <> <MySlider<number> label="Cookies to buy" value={value} onChange={setValue} /> <p> Current value: {' '} {value} </p> </> ); } Cookies to buy 25 Current value: 25 Multi thumb sliders specify their values as an array rather than a single number. function Example() { let [value, setValue] = React.useState([25, 75]); return ( <> <MySlider<number[]> label="Range" thumbLabels={['start', 'end']} value={value} onChange={setValue} /> <p>Current value: {value.join(' – ')}</p> </> ); } function Example() { let [value, setValue] = React.useState([25, 75]); return ( <> <MySlider<number[]> label="Range" thumbLabels={['start', 'end']} value={value} onChange={setValue} /> <p>Current value: {value.join(' – ')}</p> </> ); } function Example() { let [value, setValue] = React.useState([ 25, 75 ]); return ( <> <MySlider<number[]> label="Range" thumbLabels={[ 'start', 'end' ]} value={value} onChange={setValue} /> <p> Current value: {' '} {value.join( ' – ' )} </p> </> ); } Range 25 – 75 Current value: 25 – 75 ### HTML forms# Each SliderThumb supports the `name` prop for integration with HTML forms. <Slider defaultValue={50}> <Label>Opacity</Label> <SliderOutput /> <SliderTrack> <SliderThumb name="opacity" /> </SliderTrack> </Slider> <Slider defaultValue={50}> <Label>Opacity</Label> <SliderOutput /> <SliderTrack> <SliderThumb name="opacity" /> </SliderTrack> </Slider> <Slider defaultValue={50} > <Label> Opacity </Label> <SliderOutput /> <SliderTrack> <SliderThumb name="opacity" /> </SliderTrack> </Slider> Opacity 50 ## Events# * * * The `onChange` prop is called as the user drags a slider. The `onChangeEnd` prop can be used to handle when a user stops dragging. function Example() { let [value, setValue] = React.useState(25); return ( <> <MySlider<number> label="Cookies to buy" defaultValue={value} onChangeEnd={setValue} /> <p>Current value: {value}</p> </> ); } function Example() { let [value, setValue] = React.useState(25); return ( <> <MySlider<number> label="Cookies to buy" defaultValue={value} onChangeEnd={setValue} /> <p>Current value: {value}</p> </> ); } function Example() { let [value, setValue] = React.useState(25); return ( <> <MySlider<number> label="Cookies to buy" defaultValue={value} onChangeEnd={setValue} /> <p> Current value: {' '} {value} </p> </> ); } Cookies to buy 25 Current value: 25 ## Validation# * * * ### Custom value scale# By default, slider values are percentages between 0 and 100. A different scale can be used by setting the `minValue` and `maxValue` props. <MySlider label="Cookies to buy" minValue={50} maxValue={150} defaultValue={100} /> <MySlider label="Cookies to buy" minValue={50} maxValue={150} defaultValue={100} /> <MySlider label="Cookies to buy" minValue={50} maxValue={150} defaultValue={100} /> Cookies to buy 100 ### Step values# The `step` prop can be used to snap the value to certain increments. The steps are calculated starting from the minimum. For example, if `minValue={2}`, and `step={3}`, the valid step values would be 2, 5, 8, 11, etc. This example allows increments of 5 between 0 and 100. <MySlider label="Amount" formatOptions={{style: 'currency', currency: 'USD'}} minValue={0} maxValue={100} step={5} /> <MySlider label="Amount" formatOptions={{style: 'currency', currency: 'USD'}} minValue={0} maxValue={100} step={5} /> <MySlider label="Amount" formatOptions={{ style: 'currency', currency: 'USD' }} minValue={0} maxValue={100} step={5} /> Amount $0.00 ## Visual options# * * * ### Vertical orientation# Sliders are horizontally oriented by default. The `orientation` prop can be set to `"vertical"` to create a vertical slider. This example also uses `aria-label` rather than `label` to create a slider with no visible label. <MySlider orientation="vertical" aria-label="Opacity" maxValue={1} step={0.01} /> <MySlider orientation="vertical" aria-label="Opacity" maxValue={1} step={0.01} /> <MySlider orientation="vertical" aria-label="Opacity" maxValue={1} step={0.01} /> 0 Show CSS .react-aria-Slider { &[data-orientation=vertical] { height: 150px; display: block; .react-aria-Label, .react-aria-SliderOutput { display: none; } .react-aria-SliderTrack { width: 30px; height: 100%; &:before { width: 3px; height: 100%; left: 50%; transform: translateX(-50%); } } .react-aria-SliderThumb { left: 50%; } } } .react-aria-Slider { &[data-orientation=vertical] { height: 150px; display: block; .react-aria-Label, .react-aria-SliderOutput { display: none; } .react-aria-SliderTrack { width: 30px; height: 100%; &:before { width: 3px; height: 100%; left: 50%; transform: translateX(-50%); } } .react-aria-SliderThumb { left: 50%; } } } .react-aria-Slider { &[data-orientation=vertical] { height: 150px; display: block; .react-aria-Label, .react-aria-SliderOutput { display: none; } .react-aria-SliderTrack { width: 30px; height: 100%; &:before { width: 3px; height: 100%; left: 50%; transform: translateX(-50%); } } .react-aria-SliderThumb { left: 50%; } } } ### Value formatting# Values are formatted as a percentage by default, but this can be modified by using the `formatOptions` prop to specify a different format. `formatOptions` is compatible with the option parameter of Intl.NumberFormat and is applied based on the current locale. <MySlider label="Currency" formatOptions={{style: 'currency', currency: 'JPY'}} defaultValue={60} /> <MySlider label="Currency" formatOptions={{style: 'currency', currency: 'JPY'}} defaultValue={60} /> <MySlider label="Currency" formatOptions={{ style: 'currency', currency: 'JPY' }} defaultValue={60} /> Currency ¥60 ### Disabled# A slider can be disabled using the `isDisabled` prop. <MySlider label="Cookies to share" defaultValue={25} isDisabled /> <MySlider label="Cookies to share" defaultValue={25} isDisabled /> <MySlider label="Cookies to share" defaultValue={25} isDisabled /> Cookies to share 25 Show CSS .react-aria-Slider { &[data-disabled] { .react-aria-SliderTrack:before { background: var(--border-color-disabled); } .react-aria-SliderThumb { background: var(--border-color-disabled); } } } .react-aria-Slider { &[data-disabled] { .react-aria-SliderTrack:before { background: var(--border-color-disabled); } .react-aria-SliderThumb { background: var(--border-color-disabled); } } } .react-aria-Slider { &[data-disabled] { .react-aria-SliderTrack:before { background: var(--border-color-disabled); } .react-aria-SliderThumb { background: var(--border-color-disabled); } } } ## Props# * * * ### Slider# | Name | Type | Default | Description | | --- | --- | --- | --- | | `formatOptions` | `Intl.NumberFormatOptions` | — | The display format of the value label. | | `orientation` | ` Orientation ` | `'horizontal'` | The orientation of the Slider. | | `isDisabled` | `boolean` | — | Whether the whole Slider is disabled. | | `minValue` | `number` | `0` | The slider's minimum value. | | `maxValue` | `number` | `100` | The slider's maximum value. | | `step` | `number` | `1` | The slider's step value. | | `value` | `T` | — | The current value (controlled). | | `defaultValue` | `T` | — | The default value (uncontrolled). | | `children` | `ReactNode | ( (values: SliderRenderProps & & { defaultChildren: ReactNode | | undefined } )) => ReactNode` | — | The children of the component. A function may be provided to alter the children based on component state. | | `className` | `string | ( (values: SliderRenderProps & & { defaultClassName: string | | undefined } )) => string` | — | The CSS className for the element. A function may be provided to compute the class based on component state. | | `style` | `CSSProperties | ( (values: SliderRenderProps & & { defaultStyle: CSSProperties } )) => CSSProperties | undefined` | — | The inline style for the element. A function may be provided to compute the style based on component state. | Events | Name | Type | Description | | --- | --- | --- | | `onChangeEnd` | `( (value: T )) => void` | Fired when the slider stops moving, due to being let go. | | `onChange` | `( (value: T )) => void` | Handler that is called when the value changes. | Layout | Name | Type | Description | | --- | --- | --- | | `slot` | `string | null` | A slot name for the component. Slots allow the component to receive props from a parent component. An explicit `null` value indicates that the local props completely override all props received from a parent. | Accessibility | Name | Type | Description | | --- | --- | --- | | `id` | `string` | The element's unique identifier. See MDN. | | `aria-label` | `string` | Defines a string value that labels the current element. | | `aria-labelledby` | `string` | Identifies the element (or elements) that labels the current element. | | `aria-describedby` | `string` | Identifies the element (or elements) that describes the object. | | `aria-details` | `string` | Identifies the element (or elements) that provide a detailed, extended description for the object. | ### Label# A `<Label>` accepts all HTML attributes. ### SliderOutput# A `<SliderOutput>` renders the current value of the slider as text. | Name | Type | Description | | --- | --- | --- | | `children` | `ReactNode | ( (values: SliderRenderProps & & { defaultChildren: ReactNode | | undefined } )) => ReactNode` | The children of the component. A function may be provided to alter the children based on component state. | | `className` | `string | ( (values: SliderRenderProps & & { defaultClassName: string | | undefined } )) => string` | The CSS className for the element. A function may be provided to compute the class based on component state. | | `style` | `CSSProperties | ( (values: SliderRenderProps & & { defaultStyle: CSSProperties } )) => CSSProperties | undefined` | The inline style for the element. A function may be provided to compute the style based on component state. | ### SliderTrack# The `<SliderTrack>` component is a grouping of one or more `<SliderThumb>` elements. | Name | Type | Description | | --- | --- | --- | | `children` | `ReactNode | ( (values: SliderTrackRenderProps & & { defaultChildren: ReactNode | | undefined } )) => ReactNode` | The children of the component. A function may be provided to alter the children based on component state. | | `className` | `string | ( (values: SliderTrackRenderProps & & { defaultClassName: string | | undefined } )) => string` | The CSS className for the element. A function may be provided to compute the class based on component state. | | `style` | `CSSProperties | ( (values: SliderTrackRenderProps & & { defaultStyle: CSSProperties } )) => CSSProperties | undefined` | The inline style for the element. A function may be provided to compute the style based on component state. | Events | Name | Type | Description | | --- | --- | --- | | `onHoverStart` | `( (e: HoverEvent )) => void` | Handler that is called when a hover interaction starts. | | `onHoverEnd` | `( (e: HoverEvent )) => void` | Handler that is called when a hover interaction ends. | | `onHoverChange` | `( (isHovering: boolean )) => void` | Handler that is called when the hover state changes. | ### SliderThumb# The `<SliderThumb>` component renders an individual thumb within a `<SliderTrack>`. | Name | Type | Default | Description | | --- | --- | --- | --- | | `inputRef` | ` RefObject <HTMLInputElement | null>` | — | A ref for the HTML input element. | | `isDisabled` | `boolean` | — | Whether the Thumb is disabled. | | `index` | `number` | `0` | Index of the thumb within the slider. | | `autoFocus` | `boolean` | — | Whether the element should receive focus on render. | | `name` | `string` | — | The name of the input element, used when submitting an HTML form. See MDN. | | `children` | `ReactNode | ( (values: SliderThumbRenderProps & & { defaultChildren: ReactNode | | undefined } )) => ReactNode` | — | The children of the component. A function may be provided to alter the children based on component state. | | `className` | `string | ( (values: SliderThumbRenderProps & & { defaultClassName: string | | undefined } )) => string` | — | The CSS className for the element. A function may be provided to compute the class based on component state. | | `style` | `CSSProperties | ( (values: SliderThumbRenderProps & & { defaultStyle: CSSProperties } )) => CSSProperties | undefined` | — | The inline style for the element. A function may be provided to compute the style based on component state. | Events | Name | Type | Description | | --- | --- | --- | | `onFocus` | `( (e: FocusEvent<Target> )) => void` | Handler that is called when the element receives focus. | | `onBlur` | `( (e: FocusEvent<Target> )) => void` | Handler that is called when the element loses focus. | | `onFocusChange` | `( (isFocused: boolean )) => void` | Handler that is called when the element's focus status changes. | | `onKeyDown` | `( (e: KeyboardEvent )) => void` | Handler that is called when a key is pressed. | | `onKeyUp` | `( (e: KeyboardEvent )) => void` | Handler that is called when a key is released. | | `onHoverStart` | `( (e: HoverEvent )) => void` | Handler that is called when a hover interaction starts. | | `onHoverEnd` | `( (e: HoverEvent )) => void` | Handler that is called when a hover interaction ends. | | `onHoverChange` | `( (isHovering: boolean )) => void` | Handler that is called when the hover state changes. | Accessibility | Name | Type | Description | | --- | --- | --- | | `id` | `string` | The element's unique identifier. See MDN. | | `aria-label` | `string` | Defines a string value that labels the current element. | | `aria-labelledby` | `string` | Identifies the element (or elements) that labels the current element. | | `aria-describedby` | `string` | Identifies the element (or elements) that describes the object. | | `aria-details` | `string` | Identifies the element (or elements) that provide a detailed, extended description for the object. | | `aria-errormessage` | `string` | Identifies the element that provides an error message for the object. | ## Styling# * * * React Aria components can be styled in many ways, including using CSS classes, inline styles, utility classes (e.g. Tailwind), CSS-in-JS (e.g. Styled Components), etc. By default, all components include a builtin `className` attribute which can be targeted using CSS selectors. These follow the `react-aria-ComponentName` naming convention. .react-aria-Slider { /* ... */ } .react-aria-Slider { /* ... */ } .react-aria-Slider { /* ... */ } A custom `className` can also be specified on any component. This overrides the default `className` provided by React Aria with your own. <Slider className="my-slider"> {/* ... */} </Slider> <Slider className="my-slider"> {/* ... */} </Slider> <Slider className="my-slider"> {/* ... */} </Slider> In addition, some components support multiple UI states (e.g. focused, placeholder, readonly, etc.). React Aria components expose states using data attributes, which you can target in CSS selectors. For example: .react-aria-SliderThumb[data-dragging] { /* ... */ } .react-aria-SliderThumb[data-focused] { /* ... */ } .react-aria-SliderThumb[data-dragging] { /* ... */ } .react-aria-SliderThumb[data-focused] { /* ... */ } .react-aria-SliderThumb[data-dragging] { /* ... */ } .react-aria-SliderThumb[data-focused] { /* ... */ } The `className` and `style` props also accept functions which receive states for styling. This lets you dynamically determine the classes or styles to apply, which is useful when using utility CSS libraries like Tailwind. <SliderThumb className={({ isDragging }) => isDragging ? 'bg-gray-700' : 'bg-gray-600'} /> <SliderThumb className={({ isDragging }) => isDragging ? 'bg-gray-700' : 'bg-gray-600'} /> <SliderThumb className={( { isDragging } ) => isDragging ? 'bg-gray-700' : 'bg-gray-600'} /> Render props may also be used as children to alter what elements are rendered based on the current state. For example, you could implement custom formatting for the slider's current value. <SliderOutput> {state => `${state.getThumbValueLabel(0)} cookies`} </SliderOutput> <SliderOutput> {state => `${state.getThumbValueLabel(0)} cookies`} </SliderOutput> <SliderOutput> {(state) => `${ state .getThumbValueLabel( 0 ) } cookies`} </SliderOutput> The states, selectors, and render props for each component used in a `Slider` are documented below. ### Slider# The `Slider` component can be targeted with the `.react-aria-Slider` CSS selector, or by overriding with a custom `className`. It supports the following states: | Name | CSS Selector | Description | | --- | --- | --- | | `orientation` | `[data-orientation="horizontal | vertical"]` | The orientation of the slider. | | `isDisabled` | `[data-disabled]` | Whether the slider is disabled. | | `state` | `—` | State of the slider. | ### Label# A `Label` can be targeted with the `.react-aria-Label` CSS selector, or by overriding with a custom `className`. ### SliderOutput# The `SliderOutput` component can be targeted with the `.react-aria-SliderOutput` CSS selector, or by overriding with a custom `className`. It supports the following states: | Name | CSS Selector | Description | | --- | --- | --- | | `orientation` | `[data-orientation="horizontal | vertical"]` | The orientation of the slider. | | `isDisabled` | `[data-disabled]` | Whether the slider is disabled. | | `state` | `—` | State of the slider. | ### SliderTrack# The `SliderTrack` component can be targeted with the `.react-aria-SliderTrack` CSS selector, or by overriding with a custom `className`. It supports the following states: | Name | CSS Selector | Description | | --- | --- | --- | | `isHovered` | `[data-hovered]` | Whether the slider track is currently hovered with a mouse. | | `orientation` | `[data-orientation="horizontal | vertical"]` | The orientation of the slider. | | `isDisabled` | `[data-disabled]` | Whether the slider is disabled. | | `state` | `—` | State of the slider. | ### SliderThumb# The `SliderThumb` component can be targeted with the `.react-aria-SliderThumb` CSS selector, or by overriding with a custom `className`. It supports the following states: | Name | CSS Selector | Description | | --- | --- | --- | | `state` | `—` | State of the slider. | | `isDragging` | `[data-dragging]` | Whether this thumb is currently being dragged. | | `isHovered` | `[data-hovered]` | Whether the thumb is currently hovered with a mouse. | | `isFocused` | `[data-focused]` | Whether the thumb is currently focused. | | `isFocusVisible` | `[data-focus-visible]` | Whether the thumb is keyboard focused. | | `isDisabled` | `[data-disabled]` | Whether the thumb is disabled. | ## Advanced customization# * * * ### Composition# If you need to customize one of the components within a `Slider`, such as `Label` or `SliderOutput`, in many cases you can create a wrapper component. This lets you customize the props passed to the component. function MySliderOutput(props) { return <SliderOutput {...props} className="my-slider-output" /> } function MySliderOutput(props) { return ( <SliderOutput {...props} className="my-slider-output" /> ); } function MySliderOutput( props ) { return ( <SliderOutput {...props} className="my-slider-output" /> ); } ### Contexts# All React Aria Components export a corresponding context that can be used to send props to them from a parent element. This enables you to build your own compositional APIs similar to those found in React Aria Components itself. You can send any prop or ref via context that you could pass to the corresponding component. The local props and ref on the component are merged with the ones passed via context, with the local props taking precedence (following the rules documented in mergeProps). | Component | Context | Props | Ref | | --- | --- | --- | --- | | `Slider` | `SliderContext` | ` SliderProps ` | `HTMLDivElement` | This example shows a `SliderDescription` component that accepts a slider in its children and renders a description element below it. It uses the useId hook to generate a unique id for the description, and associates it with the slider via the `aria-describedby` attribute passed to the `SliderContext` provider. import {SliderContext} from 'react-aria-components'; import {useId} from 'react-aria'; interface SliderDescriptionProps { children?: React.ReactNode; description?: string; } function SliderDescription({ children, description }: SliderDescriptionProps) { let descriptionId = useId(); return ( <div> <SliderContext.Provider value={{ 'aria-describedby': descriptionId }}> {children} </SliderContext.Provider> <small id={descriptionId}>{description}</small> </div> ); } <SliderDescription description="Keeping your display on may shorten its life."> <MySlider label="Turn off display after" minValue={10} maxValue={60} defaultValue={45} formatOptions={{ style: 'unit', unit: 'minute' }} /> </SliderDescription> import {SliderContext} from 'react-aria-components'; import {useId} from 'react-aria'; interface SliderDescriptionProps { children?: React.ReactNode; description?: string; } function SliderDescription( { children, description }: SliderDescriptionProps ) { let descriptionId = useId(); return ( <div> <SliderContext.Provider value={{ 'aria-describedby': descriptionId }} > {children} </SliderContext.Provider> <small id={descriptionId}>{description}</small> </div> ); } <SliderDescription description="Keeping your display on may shorten its life."> <MySlider label="Turn off display after" minValue={10} maxValue={60} defaultValue={45} formatOptions={{ style: 'unit', unit: 'minute' }} /> </SliderDescription> import {SliderContext} from 'react-aria-components'; import {useId} from 'react-aria'; interface SliderDescriptionProps { children?: React.ReactNode; description?: string; } function SliderDescription( { children, description }: SliderDescriptionProps ) { let descriptionId = useId(); return ( <div> <SliderContext.Provider value={{ 'aria-describedby': descriptionId }} > {children} </SliderContext.Provider> <small id={descriptionId} > {description} </small> </div> ); } <SliderDescription description="Keeping your display on may shorten its life."> <MySlider label="Turn off display after" minValue={10} maxValue={60} defaultValue={45} formatOptions={{ style: 'unit', unit: 'minute' }} /> </SliderDescription> Turn off display after 45 min Keeping your display on may shorten its life. ### Custom children# Slider passes props to its child components, such as the label, via their associated contexts. These contexts are exported so you can also consume them in your own custom components. This enables you to reuse existing components from your app or component library together with React Aria Components. | Component | Context | Props | Ref | | --- | --- | --- | --- | | `Label` | `LabelContext` | ` LabelProps ` | `HTMLLabelElement` | This example consumes from `LabelContext` in an existing styled label component to make it compatible with React Aria Components. The `useContextProps` hook merges the local props and ref with the ones provided via context by Slider. import type {LabelProps} from 'react-aria-components'; import {LabelContext, useContextProps} from 'react-aria-components'; const MyCustomLabel = React.forwardRef( (props: LabelProps, ref: React.ForwardedRef<HTMLLabelElement>) => { // Merge the local props and ref with the ones provided via context. [props, ref] = useContextProps(props, ref, LabelContext); // ... your existing Label component return <label {...props} ref={ref} />; } ); import type {LabelProps} from 'react-aria-components'; import { LabelContext, useContextProps } from 'react-aria-components'; const MyCustomLabel = React.forwardRef( ( props: LabelProps, ref: React.ForwardedRef<HTMLLabelElement> ) => { // Merge the local props and ref with the ones provided via context. [props, ref] = useContextProps( props, ref, LabelContext ); // ... your existing Label component return <label {...props} ref={ref} />; } ); import type {LabelProps} from 'react-aria-components'; import { LabelContext, useContextProps } from 'react-aria-components'; const MyCustomLabel = React.forwardRef( ( props: LabelProps, ref: React.ForwardedRef< HTMLLabelElement > ) => { // Merge the local props and ref with the ones provided via context. [props, ref] = useContextProps( props, ref, LabelContext ); // ... your existing Label component return ( <label {...props} ref={ref} /> ); } ); Now you can use `MyCustomLabel` within a `Slider`, in place of the builtin React Aria Components `Label`. <Slider> <MyCustomLabel>Opacity</MyCustomLabel> <SliderTrack> <SliderThumb /> </SliderTrack> </Slider> <Slider> <MyCustomLabel>Opacity</MyCustomLabel> <SliderTrack> <SliderThumb /> </SliderTrack> </Slider> <Slider> <MyCustomLabel> Opacity </MyCustomLabel> <SliderTrack> <SliderThumb /> </SliderTrack> </Slider> ### State# Slider provides a `SliderState` object to its children via `SliderStateContext`. This can be used to access and manipulate the slider's state. This example shows a `SliderNumberField` component that can be placed within a `Slider` to allow the user to enter a number and update the slider's value. import {Input, LabelContext, NumberField, SliderStateContext, useSlottedContext} from 'react-aria-components'; function SliderNumberField() { let state = React.useContext(SliderStateContext)!; let labelProps = useSlottedContext(LabelContext)!; return ( <NumberField aria-labelledby={labelProps.id} value={state.values[0]} onChange={(v) => state.setThumbValue(0, v)} > <Input /> </NumberField> ); } <Slider defaultValue={30}> <Label>Opacity</Label> <SliderNumberField /> <SliderTrack> <SliderThumb /> </SliderTrack> </Slider> import { Input, LabelContext, NumberField, SliderStateContext, useSlottedContext } from 'react-aria-components'; function SliderNumberField() { let state = React.useContext(SliderStateContext)!; let labelProps = useSlottedContext(LabelContext)!; return ( <NumberField aria-labelledby={labelProps.id} value={state.values[0]} onChange={(v) => state.setThumbValue(0, v)} > <Input /> </NumberField> ); } <Slider defaultValue={30}> <Label>Opacity</Label> <SliderNumberField /> <SliderTrack> <SliderThumb /> </SliderTrack> </Slider> import { Input, LabelContext, NumberField, SliderStateContext, useSlottedContext } from 'react-aria-components'; function SliderNumberField() { let state = React .useContext( SliderStateContext )!; let labelProps = useSlottedContext( LabelContext )!; return ( <NumberField aria-labelledby={labelProps .id} value={state .values[0]} onChange={(v) => state .setThumbValue( 0, v )} > <Input /> </NumberField> ); } <Slider defaultValue={30} > <Label> Opacity </Label> <SliderNumberField /> <SliderTrack> <SliderThumb /> </SliderTrack> </Slider> Opacity Show CSS .react-aria-Input { border-radius: 6px; width: 3ch; } .react-aria-Input { border-radius: 6px; width: 3ch; } .react-aria-Input { border-radius: 6px; width: 3ch; } ### Hooks# If you need to customize things even further, such as accessing internal state or customizing DOM structure, you can drop down to the lower level Hook-based API. See useSlider for more details. --- ## Page: https://react-spectrum.adobe.com/react-aria/Switch.html # Switch A switch allows a user to turn a setting on or off. <table><tbody><tr><th>install</th><td><code>yarn add react-aria-components</code></td></tr><tr><th>version</th><td>1.10.1</td></tr><tr><th>usage</th><td><code><span>import</span> {Switch} <span>from</span> <span>'react-aria-components'</span></code></td></tr></tbody></table> View ARIA pattern W3C View repository GitHub View package NPM ## Example# * * * import {Switch} from 'react-aria-components'; <Switch> <div className="indicator" /> Low power mode </Switch> import {Switch} from 'react-aria-components'; <Switch> <div className="indicator" /> Low power mode </Switch> import {Switch} from 'react-aria-components'; <Switch> <div className="indicator" /> Low power mode </Switch> Low power mode Show CSS @import "@react-aria/example-theme"; .react-aria-Switch { display: flex; /* This is needed so the HiddenInput is positioned correctly */ position: relative; align-items: center; gap: 0.571rem; font-size: 1.143rem; color: var(--text-color); forced-color-adjust: none; .indicator { width: 2rem; height: 1.143rem; border: 2px solid var(--border-color); background: var(--background-color); border-radius: 1.143rem; transition: all 200ms; &:before { content: ''; display: block; margin: 0.143rem; width: 0.857rem; height: 0.857rem; background: var(--highlight-background); border-radius: 16px; transition: all 200ms; } } &[data-pressed] .indicator { border-color: var(--border-color-pressed); &:before { background: var(--highlight-background-pressed); } } &[data-selected] { .indicator { border-color: var(--highlight-background); background: var(--highlight-background); &:before { background: var(--field-background); transform: translateX(100%); } } &[data-pressed] { .indicator { border-color: var(--highlight-background-pressed); background: var(--highlight-background-pressed); } } } &[data-focus-visible] .indicator { outline: 2px solid var(--focus-ring-color); outline-offset: 2px; } } @import "@react-aria/example-theme"; .react-aria-Switch { display: flex; /* This is needed so the HiddenInput is positioned correctly */ position: relative; align-items: center; gap: 0.571rem; font-size: 1.143rem; color: var(--text-color); forced-color-adjust: none; .indicator { width: 2rem; height: 1.143rem; border: 2px solid var(--border-color); background: var(--background-color); border-radius: 1.143rem; transition: all 200ms; &:before { content: ''; display: block; margin: 0.143rem; width: 0.857rem; height: 0.857rem; background: var(--highlight-background); border-radius: 16px; transition: all 200ms; } } &[data-pressed] .indicator { border-color: var(--border-color-pressed); &:before { background: var(--highlight-background-pressed); } } &[data-selected] { .indicator { border-color: var(--highlight-background); background: var(--highlight-background); &:before { background: var(--field-background); transform: translateX(100%); } } &[data-pressed] { .indicator { border-color: var(--highlight-background-pressed); background: var(--highlight-background-pressed); } } } &[data-focus-visible] .indicator { outline: 2px solid var(--focus-ring-color); outline-offset: 2px; } } @import "@react-aria/example-theme"; .react-aria-Switch { display: flex; /* This is needed so the HiddenInput is positioned correctly */ position: relative; align-items: center; gap: 0.571rem; font-size: 1.143rem; color: var(--text-color); forced-color-adjust: none; .indicator { width: 2rem; height: 1.143rem; border: 2px solid var(--border-color); background: var(--background-color); border-radius: 1.143rem; transition: all 200ms; &:before { content: ''; display: block; margin: 0.143rem; width: 0.857rem; height: 0.857rem; background: var(--highlight-background); border-radius: 16px; transition: all 200ms; } } &[data-pressed] .indicator { border-color: var(--border-color-pressed); &:before { background: var(--highlight-background-pressed); } } &[data-selected] { .indicator { border-color: var(--highlight-background); background: var(--highlight-background); &:before { background: var(--field-background); transform: translateX(100%); } } &[data-pressed] { .indicator { border-color: var(--highlight-background-pressed); background: var(--highlight-background-pressed); } } } &[data-focus-visible] .indicator { outline: 2px solid var(--focus-ring-color); outline-offset: 2px; } } ## Features# * * * There is no native HTML element with switch styling. `<input type="checkbox">` is the closest semantically, but isn't styled or exposed to assistive technology as a switch. `Switch` helps achieve accessible switches that can be styled as needed. * **Styleable** – Hover, press, keyboard focus, and selection states are provided for easy styling. These states only apply when interacting with an appropriate input device, unlike CSS pseudo classes. * **Accessible** – Uses a visually hidden `<input>` element with `role="switch"` under the hood, which also enables HTML form integration and autofill. A label element is built-in to ensure the switch is usable with assistive technologies. * **Cross-browser** – Mouse, touch, keyboard, and focus interactions are normalized to ensure consistency across browsers and devices. ## Anatomy# * * * A switch consists of a visual selection indicator and a label. Users may click or touch a switch to toggle the selection state, or use the Tab key to navigate to it and the Space key to toggle it. In most cases, switches should have a visual label. If the switch does not have a visible label, an `aria-label` or `aria-labelledby` prop must be passed instead to identify the element to assistive technology. ## Examples# * * * Wi-Fi Switch An animated Wi-Fi Switch styled with Tailwind CSS. ## Starter kits# * * * To help kick-start your project, we offer starter kits that include example implementations of all React Aria components with various styling solutions. All components are fully styled, including support for dark mode, high contrast mode, and all UI states. Each starter comes with a pre-configured Storybook that you can experiment with, or use as a starting point for your own component library. Vanilla CSS Download ZIP Preview Tailwind CSS Download ZIP Preview ## Reusable wrappers# * * * If you will use a Switch in multiple places in your app, you can wrap all of the pieces into a reusable component. This way, the DOM structure, styling code, and other logic are defined in a single place and reused everywhere to ensure consistency. This example wraps `Switch` and all of its children together into a single component. import type {SwitchProps} from 'react-aria-components'; interface MySwitchProps extends Omit<SwitchProps, 'children'> { children: React.ReactNode } function MySwitch({children, ...props}: MySwitchProps) { return ( <Switch {...props}> <div className="indicator" /> {children} </Switch> ); } <MySwitch>Wi-Fi</MySwitch> import type {SwitchProps} from 'react-aria-components'; interface MySwitchProps extends Omit<SwitchProps, 'children'> { children: React.ReactNode; } function MySwitch({ children, ...props }: MySwitchProps) { return ( <Switch {...props}> <div className="indicator" /> {children} </Switch> ); } <MySwitch>Wi-Fi</MySwitch> import type {SwitchProps} from 'react-aria-components'; interface MySwitchProps extends Omit< SwitchProps, 'children' > { children: React.ReactNode; } function MySwitch( { children, ...props }: MySwitchProps ) { return ( <Switch {...props}> <div className="indicator" /> {children} </Switch> ); } <MySwitch> Wi-Fi </MySwitch> Wi-Fi ## Value# * * * ### Default value# Switches are not selected by default. The `defaultSelected` prop can be used to set the default state. <MySwitch defaultSelected>Wi-Fi</MySwitch> <MySwitch defaultSelected>Wi-Fi</MySwitch> <MySwitch defaultSelected > Wi-Fi </MySwitch> Wi-Fi ### Controlled value# The `isSelected` prop can be used to make the selected state controlled. The `onChange` event is fired when the user presses the switch, and receives the new value. function Example() { let [selected, setSelected] = React.useState(false); return ( <> <MySwitch isSelected={selected} onChange={setSelected}> Low power mode </MySwitch> <p>{selected ? 'Low' : 'High'} power mode active.</p> </> ); } function Example() { let [selected, setSelected] = React.useState(false); return ( <> <MySwitch isSelected={selected} onChange={setSelected} > Low power mode </MySwitch> <p>{selected ? 'Low' : 'High'} power mode active.</p> </> ); } function Example() { let [ selected, setSelected ] = React.useState( false ); return ( <> <MySwitch isSelected={selected} onChange={setSelected} > Low power mode </MySwitch> <p> {selected ? 'Low' : 'High'}{' '} power mode active. </p> </> ); } Low power mode High power mode active. ### HTML forms# Switch supports the `name` and `value` props for integration with HTML forms. <MySwitch name="power" value="low">Low power mode</MySwitch> <MySwitch name="power" value="low">Low power mode</MySwitch> <MySwitch name="power" value="low" > Low power mode </MySwitch> Low power mode ## Disabled# * * * Switches can be disabled using the `isDisabled` prop. <MySwitch isDisabled>Airplane Mode</MySwitch> <MySwitch isDisabled>Airplane Mode</MySwitch> <MySwitch isDisabled> Airplane Mode </MySwitch> Airplane Mode Show CSS .react-aria-Switch { &[data-disabled] { color: var(--text-color-disabled); .indicator { border-color: var(--border-color-disabled); &:before { background: var(--border-color-disabled); } } } } .react-aria-Switch { &[data-disabled] { color: var(--text-color-disabled); .indicator { border-color: var(--border-color-disabled); &:before { background: var(--border-color-disabled); } } } } .react-aria-Switch { &[data-disabled] { color: var(--text-color-disabled); .indicator { border-color: var(--border-color-disabled); &:before { background: var(--border-color-disabled); } } } } ### Read only# The `isReadOnly` prop makes the selection immutable. Unlike `isDisabled`, the Switch remains focusable. See the MDN docs for more information. <MySwitch isSelected isReadOnly>Bluetooth</MySwitch> <MySwitch isSelected isReadOnly>Bluetooth</MySwitch> <MySwitch isSelected isReadOnly > Bluetooth </MySwitch> Bluetooth ## Props# * * * | Name | Type | Description | | --- | --- | --- | | `inputRef` | ` RefObject <HTMLInputElement | null>` | A ref for the HTML input element. | | `defaultSelected` | `boolean` | Whether the Switch should be selected (uncontrolled). | | `isSelected` | `boolean` | Whether the Switch should be selected (controlled). | | `value` | `string` | The value of the input element, used when submitting an HTML form. See MDN. | | `isDisabled` | `boolean` | Whether the input is disabled. | | `isReadOnly` | `boolean` | Whether the input can be selected but not changed by the user. | | `autoFocus` | `boolean` | Whether the element should receive focus on render. | | `name` | `string` | The name of the input element, used when submitting an HTML form. See MDN. | | `children` | `ReactNode | ( (values: SwitchRenderProps & & { defaultChildren: ReactNode | | undefined } )) => ReactNode` | The children of the component. A function may be provided to alter the children based on component state. | | `className` | `string | ( (values: SwitchRenderProps & & { defaultClassName: string | | undefined } )) => string` | The CSS className for the element. A function may be provided to compute the class based on component state. | | `style` | `CSSProperties | ( (values: SwitchRenderProps & & { defaultStyle: CSSProperties } )) => CSSProperties | undefined` | The inline style for the element. A function may be provided to compute the style based on component state. | Events | Name | Type | Description | | --- | --- | --- | | `onChange` | `( (isSelected: boolean )) => void` | Handler that is called when the Switch's selection state changes. | | `onFocus` | `( (e: FocusEvent<Target> )) => void` | Handler that is called when the element receives focus. | | `onBlur` | `( (e: FocusEvent<Target> )) => void` | Handler that is called when the element loses focus. | | `onFocusChange` | `( (isFocused: boolean )) => void` | Handler that is called when the element's focus status changes. | | `onKeyDown` | `( (e: KeyboardEvent )) => void` | Handler that is called when a key is pressed. | | `onKeyUp` | `( (e: KeyboardEvent )) => void` | Handler that is called when a key is released. | | `onHoverStart` | `( (e: HoverEvent )) => void` | Handler that is called when a hover interaction starts. | | `onHoverEnd` | `( (e: HoverEvent )) => void` | Handler that is called when a hover interaction ends. | | `onHoverChange` | `( (isHovering: boolean )) => void` | Handler that is called when the hover state changes. | Layout | Name | Type | Description | | --- | --- | --- | | `slot` | `string | null` | A slot name for the component. Slots allow the component to receive props from a parent component. An explicit `null` value indicates that the local props completely override all props received from a parent. | Accessibility | Name | Type | Description | | --- | --- | --- | | `id` | `string` | The element's unique identifier. See MDN. | | `excludeFromTabOrder` | `boolean` | Whether to exclude the element from the sequential tab order. If true, the element will not be focusable via the keyboard by tabbing. This should be avoided except in rare scenarios where an alternative means of accessing the element or its functionality via the keyboard is available. | | `aria-controls` | `string` | Identifies the element (or elements) whose contents or presence are controlled by the current element. | | `aria-label` | `string` | Defines a string value that labels the current element. | | `aria-labelledby` | `string` | Identifies the element (or elements) that labels the current element. | | `aria-describedby` | `string` | Identifies the element (or elements) that describes the object. | | `aria-details` | `string` | Identifies the element (or elements) that provide a detailed, extended description for the object. | ## Styling# * * * React Aria components can be styled in many ways, including using CSS classes, inline styles, utility classes (e.g. Tailwind), CSS-in-JS (e.g. Styled Components), etc. By default, all components include a builtin `className` attribute which can be targeted using CSS selectors. These follow the `react-aria-ComponentName` naming convention. .react-aria-Switch { /* ... */ } .react-aria-Switch { /* ... */ } .react-aria-Switch { /* ... */ } A custom `className` can also be specified on any component. This overrides the default `className` provided by React Aria with your own. <Switch className="my-switch"> {/* ... */} </Switch> <Switch className="my-switch"> {/* ... */} </Switch> <Switch className="my-switch"> {/* ... */} </Switch> In addition, some components support multiple UI states (e.g. focused, placeholder, readonly, etc.). React Aria components expose states using data attributes, which you can target in CSS selectors. For example: .react-aria-Switch[data-pressed] { /* ... */ } .react-aria-Switch[data-pressed] { /* ... */ } .react-aria-Switch[data-pressed] { /* ... */ } The `className` and `style` props also accept functions which receive states for styling. This lets you dynamically determine the classes or styles to apply, which is useful when using utility CSS libraries like Tailwind. <Switch className={({ isPressed }) => isPressed ? 'bg-gray-700' : 'bg-gray-600'} /> <Switch className={({ isPressed }) => isPressed ? 'bg-gray-700' : 'bg-gray-600'} /> <Switch className={( { isPressed } ) => isPressed ? 'bg-gray-700' : 'bg-gray-600'} /> Render props may also be used as children to alter what elements are rendered based on the current state. For example, you could render an extra element when the switch is selected. <Switch> {({isSelected}) => ( <> {isSelected && <OnIcon />} Wi-Fi </> )} </Switch> <Switch> {({isSelected}) => ( <> {isSelected && <OnIcon />} Wi-Fi </> )} </Switch> <Switch> {( { isSelected } ) => ( <> {isSelected && ( <OnIcon /> )} Wi-Fi </> )} </Switch> The states, selectors, and render props for `Switch` are documented below. | Name | CSS Selector | Description | | --- | --- | --- | | `isSelected` | `[data-selected]` | Whether the switch is selected. | | `isHovered` | `[data-hovered]` | Whether the switch is currently hovered with a mouse. | | `isPressed` | `[data-pressed]` | Whether the switch is currently in a pressed state. | | `isFocused` | `[data-focused]` | Whether the switch is focused, either via a mouse or keyboard. | | `isFocusVisible` | `[data-focus-visible]` | Whether the switch is keyboard focused. | | `isDisabled` | `[data-disabled]` | Whether the switch is disabled. | | `isReadOnly` | `[data-readonly]` | Whether the switch is read only. | | `state` | `—` | State of the switch. | ## Advanced customization# * * * ### Contexts# All React Aria Components export a corresponding context that can be used to send props to them from a parent element. This enables you to build your own compositional APIs similar to those found in React Aria Components itself. You can send any prop or ref via context that you could pass to the corresponding component. The local props and ref on the component are merged with the ones passed via context, with the local props taking precedence (following the rules documented in mergeProps). | Component | Context | Props | Ref | | --- | --- | --- | --- | | `Switch` | `SwitchContext` | ` SwitchProps ` | `HTMLLabelElement` | This example shows a `SwitchDescription` component that accepts a switch in its children and renders a description element below it. It uses the useId hook to generate a unique id for the description, and associates it with the switch via the `aria-describedby` attribute passed to the `SwitchContext` provider. import {SwitchContext} from 'react-aria-components'; import {useId} from 'react-aria'; interface SwitchDescriptionProps { children?: React.ReactNode, description?: string } function SwitchDescription({children, description}: SwitchDescriptionProps) { let descriptionId = useId(); return ( <div> <SwitchContext.Provider value={{'aria-describedby': descriptionId}}> {children} </SwitchContext.Provider> <small id={descriptionId}>{description}</small> </div> ); } <SwitchDescription description="Connected to 'Starbucks Wifi'."> <MySwitch defaultSelected>Wi-Fi</MySwitch> </SwitchDescription> import {SwitchContext} from 'react-aria-components'; import {useId} from 'react-aria'; interface SwitchDescriptionProps { children?: React.ReactNode; description?: string; } function SwitchDescription( { children, description }: SwitchDescriptionProps ) { let descriptionId = useId(); return ( <div> <SwitchContext.Provider value={{ 'aria-describedby': descriptionId }} > {children} </SwitchContext.Provider> <small id={descriptionId}>{description}</small> </div> ); } <SwitchDescription description="Connected to 'Starbucks Wifi'."> <MySwitch defaultSelected>Wi-Fi</MySwitch> </SwitchDescription> import {SwitchContext} from 'react-aria-components'; import {useId} from 'react-aria'; interface SwitchDescriptionProps { children?: React.ReactNode; description?: string; } function SwitchDescription( { children, description }: SwitchDescriptionProps ) { let descriptionId = useId(); return ( <div> <SwitchContext.Provider value={{ 'aria-describedby': descriptionId }} > {children} </SwitchContext.Provider> <small id={descriptionId} > {description} </small> </div> ); } <SwitchDescription description="Connected to 'Starbucks Wifi'."> <MySwitch defaultSelected > Wi-Fi </MySwitch> </SwitchDescription> Wi-FiConnected to 'Starbucks Wifi'. ### Hooks# If you need to customize things further, such as accessing internal state or customizing DOM structure, you can drop down to the lower level Hook-based API. See useSwitch for more details. --- ## Page: https://react-spectrum.adobe.com/react-aria/TextField.html # TextField A text field allows a user to enter a plain text value with a keyboard. <table><tbody><tr><th>install</th><td><code>yarn add react-aria-components</code></td></tr><tr><th>version</th><td>1.10.1</td></tr><tr><th>usage</th><td><code><span>import</span> {TextField} <span>from</span> <span>'react-aria-components'</span></code></td></tr></tbody></table> View ARIA pattern W3C View repository GitHub View package NPM ## Example# * * * import {TextField, Label, Input} from 'react-aria-components'; <TextField> <Label>First name</Label> <Input /> </TextField> import { Input, Label, TextField } from 'react-aria-components'; <TextField> <Label>First name</Label> <Input /> </TextField> import { Input, Label, TextField } from 'react-aria-components'; <TextField> <Label> First name </Label> <Input /> </TextField> First name Show CSS @import "@react-aria/example-theme"; .react-aria-TextField { display: flex; flex-direction: column; width: fit-content; color: var(--text-color); .react-aria-Input, .react-aria-TextArea { padding: 0.286rem; margin: 0; border: 1px solid var(--border-color); border-radius: 6px; background: var(--field-background); font-size: 1.143rem; color: var(--field-text-color); &[data-focused] { outline: 2px solid var(--focus-ring-color); outline-offset: -1px; } } } @import "@react-aria/example-theme"; .react-aria-TextField { display: flex; flex-direction: column; width: fit-content; color: var(--text-color); .react-aria-Input, .react-aria-TextArea { padding: 0.286rem; margin: 0; border: 1px solid var(--border-color); border-radius: 6px; background: var(--field-background); font-size: 1.143rem; color: var(--field-text-color); &[data-focused] { outline: 2px solid var(--focus-ring-color); outline-offset: -1px; } } } @import "@react-aria/example-theme"; .react-aria-TextField { display: flex; flex-direction: column; width: fit-content; color: var(--text-color); .react-aria-Input, .react-aria-TextArea { padding: 0.286rem; margin: 0; border: 1px solid var(--border-color); border-radius: 6px; background: var(--field-background); font-size: 1.143rem; color: var(--field-text-color); &[data-focused] { outline: 2px solid var(--focus-ring-color); outline-offset: -1px; } } } ## Features# * * * Text fields can be built with <input> or <textarea> and <label> elements, but you must manually ensure that they are semantically connected via ids for accessibility. `TextField` helps automate this, and handle other accessibility features while allowing for custom styling. * **Accessible** – Uses a native `<input>` element. Label, description, and error message elements are automatically associated with the field. * **Validation** – Support for native HTML constraint validation with customizable UI, custom validation functions, realtime validation, and server-side validation errors. ## Anatomy# * * * Text fields consist of an input element and a label. `TextField` automatically manages the relationship between the two elements using the `for` attribute on the `<label>` element and the `aria-labelledby` attribute on the `<input>` element. `TextField` also supports optional description and error message elements, which can be used to provide more context about the field, and any validation messages. These are linked with the input via the `aria-describedby` attribute. import {TextField, Label, Input, FieldError, Text} from 'react-aria-components'; <TextField> <Label /> <Input /> <Text slot="description" /> <FieldError /> </TextField> import { FieldError, Input, Label, Text, TextField } from 'react-aria-components'; <TextField> <Label /> <Input /> <Text slot="description" /> <FieldError /> </TextField> import { FieldError, Input, Label, Text, TextField } from 'react-aria-components'; <TextField> <Label /> <Input /> <Text slot="description" /> <FieldError /> </TextField> If there is no visual label, an `aria-label` or `aria-labelledby` prop must be passed instead to identify the element to screen readers. ### Concepts# `TextField` makes use of the following concepts: Forms Validating and submitting form data, and integrating with form libraries. ### Composed components# A `TextField` uses the following components, which may also be used standalone or reused in other components. Label A label provides context for an input element. Input An input allows a user to enter a plain text value with a keyboard. ## Starter kits# * * * To help kick-start your project, we offer starter kits that include example implementations of all React Aria components with various styling solutions. All components are fully styled, including support for dark mode, high contrast mode, and all UI states. Each starter comes with a pre-configured Storybook that you can experiment with, or use as a starting point for your own component library. Vanilla CSS Download ZIP Preview Tailwind CSS Download ZIP Preview ## Reusable wrappers# * * * If you will use a TextField in multiple places in your app, you can wrap all of the pieces into a reusable component. This way, the DOM structure, styling code, and other logic are defined in a single place and reused everywhere to ensure consistency. This example wraps `TextField` and all of its children together into a single component which accepts a `label` prop, which is passed to the right place. It also shows how to use the `description` slot to render help text, and `FieldError` component to render validation errors. import type {TextFieldProps, ValidationResult} from 'react-aria-components'; import {FieldError, Text} from 'react-aria-components'; interface MyTextFieldProps extends TextFieldProps { label?: string; description?: string; errorMessage?: string | ((validation: ValidationResult) => string); } function MyTextField( { label, description, errorMessage, ...props }: MyTextFieldProps ) { return ( <TextField {...props}> <Label>{label}</Label> <Input /> {description && <Text slot="description">{description}</Text>} <FieldError>{errorMessage}</FieldError> </TextField> ); } <MyTextField label="Name" /> import type { TextFieldProps, ValidationResult } from 'react-aria-components'; import {FieldError, Text} from 'react-aria-components'; interface MyTextFieldProps extends TextFieldProps { label?: string; description?: string; errorMessage?: | string | ((validation: ValidationResult) => string); } function MyTextField( { label, description, errorMessage, ...props }: MyTextFieldProps ) { return ( <TextField {...props}> <Label>{label}</Label> <Input /> {description && ( <Text slot="description">{description}</Text> )} <FieldError>{errorMessage}</FieldError> </TextField> ); } <MyTextField label="Name" /> import type { TextFieldProps, ValidationResult } from 'react-aria-components'; import { FieldError, Text } from 'react-aria-components'; interface MyTextFieldProps extends TextFieldProps { label?: string; description?: string; errorMessage?: | string | (( validation: ValidationResult ) => string); } function MyTextField({ label, description, errorMessage, ...props }: MyTextFieldProps) { return ( <TextField {...props} > <Label> {label} </Label> <Input /> {description && ( <Text slot="description"> {description} </Text> )} <FieldError> {errorMessage} </FieldError> </TextField> ); } <MyTextField label="Name" /> Name ## Value# * * * ### Default value# A TextField's `value` is empty by default, but an initial, uncontrolled, value can be provided using the `defaultValue` prop. <MyTextField label="Email" defaultValue="me@email.com" /> <MyTextField label="Email" defaultValue="me@email.com" /> <MyTextField label="Email" defaultValue="me@email.com" /> Email ### Controlled value# The `value` prop can be used to make the value controlled. The `onChange` event is fired when the user edits the text, and receives the new value. function Example() { let [text, setText] = React.useState(''); return ( <> <MyTextField label="Your text" onChange={setText} /> <p>Mirrored text: {text}</p> </> ); } function Example() { let [text, setText] = React.useState(''); return ( <> <MyTextField label="Your text" onChange={setText} /> <p>Mirrored text: {text}</p> </> ); } function Example() { let [text, setText] = React.useState(''); return ( <> <MyTextField label="Your text" onChange={setText} /> <p> Mirrored text: {' '} {text} </p> </> ); } Your text Mirrored text: ### HTML forms# TextField supports the `name` prop for integration with HTML forms. In addition, attributes such as `type`, `pattern`, `inputMode`, and others are passed through to the underlying `<input>` element. <MyTextField label="Email" name="email" type="email" /> <MyTextField label="Email" name="email" type="email" /> <MyTextField label="Email" name="email" type="email" /> Email ## Validation# * * * TextField supports HTML constraint validation props such as `isRequired`, `type="email"`, `minLength`, and `pattern`, as well as custom validation functions, realtime validation, and server-side validation. It can also be integrated with other form libraries. See the Forms guide to learn more. To display validation errors, add a `<FieldError>` element as a child of the TextField. This allows you to render error messages from all of the above sources with consistent custom styles. import {Form, FieldError, Button} from 'react-aria-components'; <Form> <TextField name="email" type="email" isRequired> <Label>Email</Label> <Input /> <FieldError /> </TextField> <Button type="submit">Submit</Button> </Form> import { Button, FieldError, Form } from 'react-aria-components'; <Form> <TextField name="email" type="email" isRequired> <Label>Email</Label> <Input /> <FieldError /> </TextField> <Button type="submit">Submit</Button> </Form> import { Button, FieldError, Form } from 'react-aria-components'; <Form> <TextField name="email" type="email" isRequired > <Label> Email </Label> <Input /> <FieldError /> </TextField> <Button type="submit"> Submit </Button> </Form> Email Submit Show CSS .react-aria-TextField { .react-aria-Input, .react-aria-TextArea { &[data-invalid] { border-color: var(--invalid-color); } } .react-aria-FieldError { font-size: 12px; color: var(--invalid-color); } } .react-aria-TextField { .react-aria-Input, .react-aria-TextArea { &[data-invalid] { border-color: var(--invalid-color); } } .react-aria-FieldError { font-size: 12px; color: var(--invalid-color); } } .react-aria-TextField { .react-aria-Input, .react-aria-TextArea { &[data-invalid] { border-color: var(--invalid-color); } } .react-aria-FieldError { font-size: 12px; color: var(--invalid-color); } } By default, `FieldError` displays default validation messages provided by the browser. See Customizing error messages in the Forms guide to learn how to provide your own custom errors. ### Description# The `description` slot can be used to associate additional help text with a text field. <TextField> <Label>Email</Label> <Input /> <Text slot="description"> Enter an email for us to contact you about your order. </Text></TextField> <TextField> <Label>Email</Label> <Input /> <Text slot="description"> Enter an email for us to contact you about your order. </Text></TextField> <TextField> <Label>Email</Label> <Input /> <Text slot="description"> Enter an email for us to contact you about your order. </Text></TextField> EmailEnter an email for us to contact you about your order. Show CSS .react-aria-TextField { [slot=description] { font-size: 12px; } } .react-aria-TextField { [slot=description] { font-size: 12px; } } .react-aria-TextField { [slot=description] { font-size: 12px; } } ## Disabled# * * * A TextField can be disabled using the `isDisabled` prop. <MyTextField label="Email" isDisabled /> <MyTextField label="Email" isDisabled /> <MyTextField label="Email" isDisabled /> Email Show CSS .react-aria-TextField { .react-aria-Input, .react-aria-TextArea { &[data-disabled] { border-color: var(--border-color-disabled); color: var(--text-color-disabled); } } } .react-aria-TextField { .react-aria-Input, .react-aria-TextArea { &[data-disabled] { border-color: var(--border-color-disabled); color: var(--text-color-disabled); } } } .react-aria-TextField { .react-aria-Input, .react-aria-TextArea { &[data-disabled] { border-color: var(--border-color-disabled); color: var(--text-color-disabled); } } } ### Read only# The `isReadOnly` boolean prop makes the TextField's text content immutable. Unlike `isDisabled`, the TextField remains focusable and the contents can still be copied. See the MDN docs for more information. <MyTextField label="Email" defaultValue="abc@adobe.com" isReadOnly /> <MyTextField label="Email" defaultValue="abc@adobe.com" isReadOnly /> <MyTextField label="Email" defaultValue="abc@adobe.com" isReadOnly /> Email ## Multi-line# * * * TextField supports using the `TextArea` component in place of `Input` for multi-line text input. import {TextField, Label, TextArea} from 'react-aria-components'; <TextField> <Label>Comment</Label> <TextArea /> </TextField> import { Label, TextArea, TextField } from 'react-aria-components'; <TextField> <Label>Comment</Label> <TextArea /> </TextField> import { Label, TextArea, TextField } from 'react-aria-components'; <TextField> <Label> Comment </Label> <TextArea /> </TextField> Comment ## Props# * * * ### TextField# | Name | Type | Default | Description | | --- | --- | --- | --- | | `isInvalid` | `boolean` | — | Whether the value is invalid. | | `enterKeyHint` | `'enter' | 'done' | 'go' | 'next' | 'previous' | 'search' | 'send'` | — | An enumerated attribute that defines what action label or icon to preset for the enter key on virtual keyboards. See MDN. | | `isDisabled` | `boolean` | — | Whether the input is disabled. | | `isReadOnly` | `boolean` | — | Whether the input can be selected but not changed by the user. | | `isRequired` | `boolean` | — | Whether user input is required on the input before form submission. | | `validate` | `( (value: string )) => ValidationError | true | null | undefined` | — | A function that returns an error message if a given value is invalid. Validation errors are displayed to the user when the form is submitted if `validationBehavior="native"`. For realtime validation, use the `isInvalid` prop instead. | | `autoFocus` | `boolean` | — | Whether the element should receive focus on render. | | `value` | `string` | — | The current value (controlled). | | `defaultValue` | `string` | — | The default value (uncontrolled). | | `autoComplete` | `string` | — | Describes the type of autocomplete functionality the input should provide if any. See MDN. | | `maxLength` | `number` | — | The maximum number of characters supported by the input. See MDN. | | `minLength` | `number` | — | The minimum number of characters required by the input. See MDN. | | `pattern` | `string` | — | Regex pattern that the value of the input must match to be valid. See MDN. | | `type` | `'text' | 'search' | 'url' | 'tel' | 'email' | 'password' | string & & {}` | — | The type of input to render. See MDN. | | `inputMode` | `'none' | 'text' | 'tel' | 'url' | 'email' | 'numeric' | 'decimal' | 'search'` | — | Hints at the type of data that might be entered by the user while editing the element or its contents. See MDN. | | `autoCorrect` | `string` | — | An attribute that takes as its value a space-separated string that describes what, if any, type of autocomplete functionality the input should provide. See MDN. | | `spellCheck` | `string` | — | An enumerated attribute that defines whether the element may be checked for spelling errors. See MDN. | | `name` | `string` | — | The name of the input element, used when submitting an HTML form. See MDN. | | `validationBehavior` | `'native' | 'aria'` | `'native'` | Whether to use native HTML form validation to prevent form submission when the value is missing or invalid, or mark the field as required or invalid via ARIA. | | `children` | `ReactNode | ( (values: TextFieldRenderProps & & { defaultChildren: ReactNode | | undefined } )) => ReactNode` | — | The children of the component. A function may be provided to alter the children based on component state. | | `className` | `string | ( (values: TextFieldRenderProps & & { defaultClassName: string | | undefined } )) => string` | — | The CSS className for the element. A function may be provided to compute the class based on component state. | | `style` | `CSSProperties | ( (values: TextFieldRenderProps & & { defaultStyle: CSSProperties } )) => CSSProperties | undefined` | — | The inline style for the element. A function may be provided to compute the style based on component state. | Events | Name | Type | Description | | --- | --- | --- | | `onFocus` | `( (e: FocusEvent<T> )) => void` | Handler that is called when the element receives focus. | | `onBlur` | `( (e: FocusEvent<T> )) => void` | Handler that is called when the element loses focus. | | `onFocusChange` | `( (isFocused: boolean )) => void` | Handler that is called when the element's focus status changes. | | `onKeyDown` | `( (e: KeyboardEvent )) => void` | Handler that is called when a key is pressed. | | `onKeyUp` | `( (e: KeyboardEvent )) => void` | Handler that is called when a key is released. | | `onChange` | `( (value: T )) => void` | Handler that is called when the value changes. | | `onCopy` | `ClipboardEventHandler<HTMLInputElement>` | Handler that is called when the user copies text. See MDN. | | `onCut` | `ClipboardEventHandler<HTMLInputElement>` | Handler that is called when the user cuts text. See MDN. | | `onPaste` | `ClipboardEventHandler<HTMLInputElement>` | Handler that is called when the user pastes text. See MDN. | | `onCompositionStart` | `CompositionEventHandler<HTMLInputElement>` | Handler that is called when a text composition system starts a new text composition session. See MDN. | | `onCompositionEnd` | `CompositionEventHandler<HTMLInputElement>` | Handler that is called when a text composition system completes or cancels the current text composition session. See MDN. | | `onCompositionUpdate` | `CompositionEventHandler<HTMLInputElement>` | Handler that is called when a new character is received in the current text composition session. See MDN. | | `onSelect` | `ReactEventHandler<HTMLInputElement>` | Handler that is called when text in the input is selected. See MDN. | | `onBeforeInput` | `FormEventHandler<HTMLInputElement>` | Handler that is called when the input value is about to be modified. See MDN. | | `onInput` | `FormEventHandler<HTMLInputElement>` | Handler that is called when the input value is modified. See MDN. | Layout | Name | Type | Description | | --- | --- | --- | | `slot` | `string | null` | A slot name for the component. Slots allow the component to receive props from a parent component. An explicit `null` value indicates that the local props completely override all props received from a parent. | Accessibility | Name | Type | Description | | --- | --- | --- | | `id` | `string` | The element's unique identifier. See MDN. | | `excludeFromTabOrder` | `boolean` | Whether to exclude the element from the sequential tab order. If true, the element will not be focusable via the keyboard by tabbing. This should be avoided except in rare scenarios where an alternative means of accessing the element or its functionality via the keyboard is available. | | `aria-activedescendant` | `string` | Identifies the currently active element when DOM focus is on a composite widget, textbox, group, or application. | | `aria-autocomplete` | `'none' | 'inline' | 'list' | 'both'` | Indicates whether inputting text could trigger display of one or more predictions of the user's intended value for an input and specifies how predictions would be presented if they are made. | | `aria-haspopup` | `boolean | 'false' | 'true' | 'menu' | 'listbox' | 'tree' | 'grid' | 'dialog'` | Indicates the availability and type of interactive popup element, such as menu or dialog, that can be triggered by an element. | | `aria-controls` | `string` | Identifies the element (or elements) whose contents or presence are controlled by the current element. | | `aria-label` | `string` | Defines a string value that labels the current element. | | `aria-labelledby` | `string` | Identifies the element (or elements) that labels the current element. | | `aria-describedby` | `string` | Identifies the element (or elements) that describes the object. | | `aria-details` | `string` | Identifies the element (or elements) that provide a detailed, extended description for the object. | | `aria-errormessage` | `string` | Identifies the element that provides an error message for the object. | ### Label# A `<Label>` accepts all HTML attributes. ### Input# An `<Input>` accepts all HTML attributes. ### TextArea# A `<TextArea>` accepts all HTML attributes. ### Text# `<Text>` accepts all HTML attributes. ### FieldError# A `<FieldError>` displays validation errors. Show props | Name | Type | Description | | --- | --- | --- | | `children` | `ReactNode | ( (values: FieldErrorRenderProps & & { defaultChildren: ReactNode | | undefined } )) => ReactNode` | The children of the component. A function may be provided to alter the children based on component state. | | `className` | `string | ( (values: FieldErrorRenderProps & & { defaultClassName: string | | undefined } )) => string` | The CSS className for the element. A function may be provided to compute the class based on component state. | | `style` | `CSSProperties | ( (values: FieldErrorRenderProps & & { defaultStyle: CSSProperties } )) => CSSProperties | undefined` | The inline style for the element. A function may be provided to compute the style based on component state. | Accessibility | Name | Type | Description | | --- | --- | --- | | `id` | `string` | The element's unique identifier. See MDN. | ## Styling# * * * React Aria components can be styled in many ways, including using CSS classes, inline styles, utility classes (e.g. Tailwind), CSS-in-JS (e.g. Styled Components), etc. By default, all components include a builtin `className` attribute which can be targeted using CSS selectors. These follow the `react-aria-ComponentName` naming convention. .react-aria-TextField { /* ... */ } .react-aria-TextField { /* ... */ } .react-aria-TextField { /* ... */ } A custom `className` can also be specified on any component. This overrides the default `className` provided by React Aria with your own. <TextField className="my-textfield"> {/* ... */} </TextField> <TextField className="my-textfield"> {/* ... */} </TextField> <TextField className="my-textfield"> {/* ... */} </TextField> In addition, some components support multiple UI states (e.g. focused, placeholder, readonly, etc.). React Aria components expose states using data attributes, which you can target in CSS selectors. For example: input[data-hovered] { /* ... */ } input[data-disabled] { /* ... */ } input[data-hovered] { /* ... */ } input[data-disabled] { /* ... */ } input[data-hovered] { /* ... */ } input[data-disabled] { /* ... */ } The states, selectors, and render props for each component used in a `TextField` are documented below. ### TextField# A `TextField` can be targeted with the `.react-aria-TextField` CSS selector, or by overriding with a custom `className`. It supports the following states: | Name | CSS Selector | Description | | --- | --- | --- | | `isDisabled` | `[data-disabled]` | Whether the text field is disabled. | | `isInvalid` | `[data-invalid]` | Whether the value is invalid. | | `isReadOnly` | `[data-readonly]` | Whether the text field is read only. | | `isRequired` | `[data-required]` | Whether the text field is required. | ### Label# A `Label` can be targeted with the `.react-aria-Label` CSS selector, or by overriding with a custom `className`. ### Input# An `Input` can be targeted with the `.react-aria-Input` CSS selector, or by overriding with a custom `className`. It supports the following states: | Name | CSS Selector | Description | | --- | --- | --- | | `isHovered` | `[data-hovered]` | Whether the input is currently hovered with a mouse. | | `isFocused` | `[data-focused]` | Whether the input is focused, either via a mouse or keyboard. | | `isFocusVisible` | `[data-focus-visible]` | Whether the input is keyboard focused. | | `isDisabled` | `[data-disabled]` | Whether the input is disabled. | | `isInvalid` | `[data-invalid]` | Whether the input is invalid. | ### TextArea# A `TextArea` can be targeted with the `.react-aria-TextArea` CSS selector, or by overriding with a custom `className`. It supports the same states as `Input` described above. ### Text# The help text elements within a `TextField` can be targeted with the `[slot=description]` and `[slot=errorMessage]` CSS selectors, or by adding a custom `className`. ### FieldError# A `FieldError` can be targeted with the `.react-aria-FieldError` CSS selector, or by overriding with a custom `className`. It supports the following render props: | Name | Description | | --- | --- | | `isInvalid` | Whether the input value is invalid. | | `validationErrors` | The current error messages for the input if it is invalid, otherwise an empty array. | | `validationDetails` | The native validation details for the input. | ## Advanced customization# * * * ### Composition# If you need to customize one of the components within a `TextField`, such as `Label` or `Input`, in many cases you can create a wrapper component. This lets you customize the props passed to the component. function MyInput(props) { return <Input {...props} className="my-input" /> } function MyInput(props) { return <Input {...props} className="my-input" /> } function MyInput(props) { return ( <Input {...props} className="my-input" /> ); } ### Contexts# All React Aria Components export a corresponding context that can be used to send props to them from a parent element. This enables you to build your own compositional APIs similar to those found in React Aria Components itself. You can send any prop or ref via context that you could pass to the corresponding component. The local props and ref on the component are merged with the ones passed via context, with the local props taking precedence (following the rules documented in mergeProps). | Component | Context | Props | Ref | | --- | --- | --- | --- | | `TextField` | `TextFieldContext` | ` TextFieldProps ` | `HTMLDivElement` | This example shows a `FieldGroup` component that renders a group of text fields with a title and optional error message. It uses the useId hook to generate a unique id for the error message. All of the child TextFields are marked invalid and associated with the error message via the `aria-describedby` attribute passed to the `TextFieldContext` provider. import {TextFieldContext} from 'react-aria-components'; import {useId} from 'react-aria'; interface FieldGroupProps { title?: string; children?: React.ReactNode; errorMessage?: string; } function FieldGroup({ title, children, errorMessage }: FieldGroupProps) { let errorId = useId(); return ( <fieldset> <legend>{title}</legend> <TextFieldContext.Provider value={{ isInvalid: !!errorMessage, 'aria-describedby': errorMessage ? errorId : undefined }} > {children} </TextFieldContext.Provider> {errorMessage && ( <small id={errorId} className="invalid">{errorMessage}</small> )} </fieldset> ); } <FieldGroup title="Account details" errorMessage="Invalid account details."> <MyTextField label="Name" defaultValue="Devon" /> <MyTextField label="Email" defaultValue="devon@example.com" /> </FieldGroup> import {TextFieldContext} from 'react-aria-components'; import {useId} from 'react-aria'; interface FieldGroupProps { title?: string; children?: React.ReactNode; errorMessage?: string; } function FieldGroup( { title, children, errorMessage }: FieldGroupProps ) { let errorId = useId(); return ( <fieldset> <legend>{title}</legend> <TextFieldContext.Provider value={{ isInvalid: !!errorMessage, 'aria-describedby': errorMessage ? errorId : undefined }} > {children} </TextFieldContext.Provider> {errorMessage && ( <small id={errorId} className="invalid"> {errorMessage} </small> )} </fieldset> ); } <FieldGroup title="Account details" errorMessage="Invalid account details." > <MyTextField label="Name" defaultValue="Devon" /> <MyTextField label="Email" defaultValue="devon@example.com" /> </FieldGroup> import {TextFieldContext} from 'react-aria-components'; import {useId} from 'react-aria'; interface FieldGroupProps { title?: string; children?: React.ReactNode; errorMessage?: string; } function FieldGroup( { title, children, errorMessage }: FieldGroupProps ) { let errorId = useId(); return ( <fieldset> <legend> {title} </legend> <TextFieldContext.Provider value={{ isInvalid: !!errorMessage, 'aria-describedby': errorMessage ? errorId : undefined }} > {children} </TextFieldContext.Provider> {errorMessage && ( <small id={errorId} className="invalid" > {errorMessage} </small> )} </fieldset> ); } <FieldGroup title="Account details" errorMessage="Invalid account details." > <MyTextField label="Name" defaultValue="Devon" /> <MyTextField label="Email" defaultValue="devon@example.com" /> </FieldGroup> Account details Name Email Invalid account details. Show CSS fieldset { padding: 1.5em; width: fit-content; } .invalid { color: var(--invalid-color); margin-top: 1em; display: block; } fieldset { padding: 1.5em; width: fit-content; } .invalid { color: var(--invalid-color); margin-top: 1em; display: block; } fieldset { padding: 1.5em; width: fit-content; } .invalid { color: var(--invalid-color); margin-top: 1em; display: block; } ### Custom children# TextField passes props to its child components, such as the label and input, via their associated contexts. These contexts are exported so you can also consume them in your own custom components. This enables you to reuse existing components from your app or component library together with React Aria Components. | Component | Context | Props | Ref | | --- | --- | --- | --- | | `Label` | `LabelContext` | ` LabelProps ` | `HTMLLabelElement` | | `Input` | `InputContext` | ` InputProps ` | `HTMLInputElement` | | `TextArea` | `TextAreaContext` | ` TextAreaProps ` | `HTMLTextAreaElement` | | `Text` | `TextContext` | ` TextProps ` | `HTMLElement` | This example consumes from `LabelContext` in an existing styled label component to make it compatible with React Aria Components. The `useContextProps` hook merges the local props and ref with the ones provided via context by TextField. import type {LabelProps} from 'react-aria-components'; import {LabelContext, useContextProps} from 'react-aria-components'; const MyCustomLabel = React.forwardRef( (props: LabelProps, ref: React.ForwardedRef<HTMLLabelElement>) => { // Merge the local props and ref with the ones provided via context. [props, ref] = useContextProps(props, ref, LabelContext); // ... your existing Label component return <label {...props} ref={ref} />; } ); import type {LabelProps} from 'react-aria-components'; import { LabelContext, useContextProps } from 'react-aria-components'; const MyCustomLabel = React.forwardRef( ( props: LabelProps, ref: React.ForwardedRef<HTMLLabelElement> ) => { // Merge the local props and ref with the ones provided via context. [props, ref] = useContextProps( props, ref, LabelContext ); // ... your existing Label component return <label {...props} ref={ref} />; } ); import type {LabelProps} from 'react-aria-components'; import { LabelContext, useContextProps } from 'react-aria-components'; const MyCustomLabel = React.forwardRef( ( props: LabelProps, ref: React.ForwardedRef< HTMLLabelElement > ) => { // Merge the local props and ref with the ones provided via context. [props, ref] = useContextProps( props, ref, LabelContext ); // ... your existing Label component return ( <label {...props} ref={ref} /> ); } ); Now you can use `MyCustomLabel` within a `TextField`, in place of the builtin React Aria Components `Label`. <TextField> <MyCustomLabel>Name</MyCustomLabel> <Input /> </TextField> <TextField> <MyCustomLabel>Name</MyCustomLabel> <Input /> </TextField> <TextField> <MyCustomLabel> Name </MyCustomLabel> <Input /> </TextField> ### Hooks# If you need to customize things even further, such as accessing internal state or intercepting events, you can drop down to the lower level Hook-based API. See useTextField for more details. --- ## Page: https://react-spectrum.adobe.com/react-aria/Breadcrumbs.html # Breadcrumbs Breadcrumbs display a hierarchy of links to the current page or resource in an application. <table><tbody><tr><th>install</th><td><code>yarn add react-aria-components</code></td></tr><tr><th>version</th><td>1.10.1</td></tr><tr><th>usage</th><td><code><span>import</span> {Breadcrumbs} <span>from</span> <span>'react-aria-components'</span></code></td></tr></tbody></table> View ARIA pattern W3C View repository GitHub View package NPM ## Example# * * * import {Breadcrumbs, Breadcrumb, Link} from 'react-aria-components'; <Breadcrumbs> <Breadcrumb><Link href="/">Home</Link></Breadcrumb> <Breadcrumb><Link href="/react-aria/">React Aria</Link></Breadcrumb> <Breadcrumb><Link>Breadcrumbs</Link></Breadcrumb> </Breadcrumbs> import { Breadcrumb, Breadcrumbs, Link } from 'react-aria-components'; <Breadcrumbs> <Breadcrumb> <Link href="/">Home</Link> </Breadcrumb> <Breadcrumb> <Link href="/react-aria/">React Aria</Link> </Breadcrumb> <Breadcrumb> <Link>Breadcrumbs</Link> </Breadcrumb> </Breadcrumbs> import { Breadcrumb, Breadcrumbs, Link } from 'react-aria-components'; <Breadcrumbs> <Breadcrumb> <Link href="/"> Home </Link> </Breadcrumb> <Breadcrumb> <Link href="/react-aria/"> React Aria </Link> </Breadcrumb> <Breadcrumb> <Link> Breadcrumbs </Link> </Breadcrumb> </Breadcrumbs> 1. Home 2. React Aria 3. Breadcrumbs Show CSS @import "@react-aria/example-theme"; .react-aria-Breadcrumbs { display: flex; align-items: center; list-style: none; margin: 0; padding: 0; font-size: 18px; color: var(--text-color); .react-aria-Breadcrumb:not(:last-child)::after { content: '›'; content: '›' / ''; alt: ' '; padding: 0 5px; } .react-aria-Link { color: var(--link-color-secondary); outline: none; position: relative; text-decoration: none; cursor: pointer; &[data-hovered] { text-decoration: underline; } &[data-current] { color: var(--text-color); font-weight: bold; } &[data-focus-visible]:after { content: ''; position: absolute; inset: -2px -4px; border-radius: 6px; border: 2px solid var(--focus-ring-color); } } } @import "@react-aria/example-theme"; .react-aria-Breadcrumbs { display: flex; align-items: center; list-style: none; margin: 0; padding: 0; font-size: 18px; color: var(--text-color); .react-aria-Breadcrumb:not(:last-child)::after { content: '›'; content: '›' / ''; alt: ' '; padding: 0 5px; } .react-aria-Link { color: var(--link-color-secondary); outline: none; position: relative; text-decoration: none; cursor: pointer; &[data-hovered] { text-decoration: underline; } &[data-current] { color: var(--text-color); font-weight: bold; } &[data-focus-visible]:after { content: ''; position: absolute; inset: -2px -4px; border-radius: 6px; border: 2px solid var(--focus-ring-color); } } } @import "@react-aria/example-theme"; .react-aria-Breadcrumbs { display: flex; align-items: center; list-style: none; margin: 0; padding: 0; font-size: 18px; color: var(--text-color); .react-aria-Breadcrumb:not(:last-child)::after { content: '›'; content: '›' / ''; alt: ' '; padding: 0 5px; } .react-aria-Link { color: var(--link-color-secondary); outline: none; position: relative; text-decoration: none; cursor: pointer; &[data-hovered] { text-decoration: underline; } &[data-current] { color: var(--text-color); font-weight: bold; } &[data-focus-visible]:after { content: ''; position: absolute; inset: -2px -4px; border-radius: 6px; border: 2px solid var(--focus-ring-color); } } } ## Features# * * * Breadcrumbs provide a list of links to parent pages of the current page in hierarchical order. `Breadcrumbs` helps implement these in an accessible way. * **Flexible** – Support for HTML navigation links, JavaScript handled links, and client side routing. * **Accessible** – Implemented as an ordered list of links. The last link is automatically marked as the current page using `aria-current`. * **Styleable** – Hover, press, and keyboard focus states are provided for easy styling. These states only apply when interacting with an appropriate input device, unlike CSS pseudo classes. ## Anatomy# * * * Breadcrumbs consist of a list of links, typically with a visual separator icon between each item. The last link represents the current page in the hierarchy, with the previous links representing the parent pages of the current page. Each of these parent links can be clicked, tapped, or triggered via the Enter key to navigate to that page. import {Breadcrumbs, Breadcrumb, Link} from 'react-aria-components'; <Breadcrumbs> <Breadcrumb><Link /></Breadcrumb> </Breadcrumbs> import { Breadcrumb, Breadcrumbs, Link } from 'react-aria-components'; <Breadcrumbs> <Breadcrumb> <Link /> </Breadcrumb> </Breadcrumbs> import { Breadcrumb, Breadcrumbs, Link } from 'react-aria-components'; <Breadcrumbs> <Breadcrumb> <Link /> </Breadcrumb> </Breadcrumbs> ### Concepts# `Breadcrumbs` makes use of the following concepts: Collections Defining collections of items, async loading, and updating items over time. ### Composed components# `Breadcrumbs` uses the following components, which may also be used standalone or reused in other components. Link A link allows a user to navigate to another page. ## Starter kits# * * * To help kick-start your project, we offer starter kits that include example implementations of all React Aria components with various styling solutions. All components are fully styled, including support for dark mode, high contrast mode, and all UI states. Each starter comes with a pre-configured Storybook that you can experiment with, or use as a starting point for your own component library. Vanilla CSS Download ZIP Preview Tailwind CSS Download ZIP Preview ## Content# * * * `Breadcrumbs` follows the Collection Components API, accepting both static and dynamic collections. The examples above show static collections, which can be used when the full list of options is known ahead of time. Dynamic collections, as shown below, can be used when the options come from an external data source such as an API call, or update over time. As seen below, an iterable list of options is passed to the Breadcrumbs using the `items` prop. A function provided as `children` of the `<Breadcrumbs>` is called to render each item. When a breadcrumb is pressed, the `onAction` event is triggered and the breadcrumbs array is updated. import type {Key} from 'react-aria-components'; function Example() { let [breadcrumbs, setBreadcrumbs] = React.useState([ {id: 1, label: 'Home'}, {id: 2, label: 'Trendy'}, {id: 3, label: 'March 2022 Assets'} ]); let navigate = (id: Key) => { let i = breadcrumbs.findIndex(item => item.id === id); setBreadcrumbs(breadcrumbs.slice(0, i + 1)); }; return ( <Breadcrumbs items={breadcrumbs} onAction={navigate}> {item => <Breadcrumb><Link>{item.label}</Link></Breadcrumb>} </Breadcrumbs> ); } import type {Key} from 'react-aria-components'; function Example() { let [breadcrumbs, setBreadcrumbs] = React.useState([ { id: 1, label: 'Home' }, { id: 2, label: 'Trendy' }, { id: 3, label: 'March 2022 Assets' } ]); let navigate = (id: Key) => { let i = breadcrumbs.findIndex((item) => item.id === id); setBreadcrumbs(breadcrumbs.slice(0, i + 1)); }; return ( <Breadcrumbs items={breadcrumbs} onAction={navigate}> {(item) => ( <Breadcrumb> <Link>{item.label}</Link> </Breadcrumb> )} </Breadcrumbs> ); } import type {Key} from 'react-aria-components'; function Example() { let [ breadcrumbs, setBreadcrumbs ] = React.useState([ { id: 1, label: 'Home' }, { id: 2, label: 'Trendy' }, { id: 3, label: 'March 2022 Assets' } ]); let navigate = ( id: Key ) => { let i = breadcrumbs .findIndex( (item) => item.id === id ); setBreadcrumbs( breadcrumbs.slice( 0, i + 1 ) ); }; return ( <Breadcrumbs items={breadcrumbs} onAction={navigate} > {(item) => ( <Breadcrumb> <Link> {item.label} </Link> </Breadcrumb> )} </Breadcrumbs> ); } 1. Home 2. Trendy 3. March 2022 Assets ### Client side routing# The `<Link>` component works with frameworks and client side routers like Next.js and React Router. As with other React Aria components that support links, this works via the `RouterProvider` component at the root of your app. See the client side routing guide to learn how to set this up. ## Separator icons# * * * The above examples use the CSS `:after` pseudo class to add separators between each item. These may also be DOM elements instead, e.g. SVG icons. Be sure that they have `aria-hidden="true"` so they are hidden from assistive technologies. import ChevronIcon from '@spectrum-icons/workflow/ChevronDoubleRight'; <Breadcrumbs> <Breadcrumb className="my-item"> <Link href="/">Home</Link> <ChevronIcon size="S" /> </Breadcrumb> <Breadcrumb><Link>React Aria</Link></Breadcrumb> </Breadcrumbs> import ChevronIcon from '@spectrum-icons/workflow/ChevronDoubleRight'; <Breadcrumbs> <Breadcrumb className="my-item"> <Link href="/">Home</Link> <ChevronIcon size="S" /> </Breadcrumb> <Breadcrumb> <Link>React Aria</Link> </Breadcrumb> </Breadcrumbs> import ChevronIcon from '@spectrum-icons/workflow/ChevronDoubleRight'; <Breadcrumbs> <Breadcrumb className="my-item"> <Link href="/"> Home </Link> <ChevronIcon size="S" /> </Breadcrumb> <Breadcrumb> <Link> React Aria </Link> </Breadcrumb> </Breadcrumbs> 1. Home 2. React Aria Show CSS .my-item svg { vertical-align: middle; padding: 0 5px; margin-top: -2px; } .my-item svg { vertical-align: middle; padding: 0 5px; margin-top: -2px; } .my-item svg { vertical-align: middle; padding: 0 5px; margin-top: -2px; } ## Landmarks# * * * When breadcrumbs are used as a main navigation element for a page, they can be placed in a navigation landmark. Landmarks help assistive technology users quickly find major sections of a page. Place breadcrumbs inside a `<nav>` element with an `aria-label` to create a navigation landmark. <nav aria-label="Breadcrumbs"> <Breadcrumbs> <Breadcrumb><Link href="/">Home</Link></Breadcrumb> <Breadcrumb><Link href="/react-aria/">React Aria</Link></Breadcrumb> <Breadcrumb><Link>Breadcrumbs</Link></Breadcrumb> </Breadcrumbs> </nav> <nav aria-label="Breadcrumbs"> <Breadcrumbs> <Breadcrumb> <Link href="/">Home</Link> </Breadcrumb> <Breadcrumb> <Link href="/react-aria/">React Aria</Link> </Breadcrumb> <Breadcrumb> <Link>Breadcrumbs</Link> </Breadcrumb> </Breadcrumbs> </nav> <nav aria-label="Breadcrumbs"> <Breadcrumbs> <Breadcrumb> <Link href="/"> Home </Link> </Breadcrumb> <Breadcrumb> <Link href="/react-aria/"> React Aria </Link> </Breadcrumb> <Breadcrumb> <Link> Breadcrumbs </Link> </Breadcrumb> </Breadcrumbs> </nav> 1. Home 2. React Aria 3. Breadcrumbs It is best to keep the number of landmarks on a page to a minimum, so only place breadcrumbs in a navigation landmark when it represents the primary or secondary navigation for the page. For example, breadcrumbs within table rows or popovers most likely should not be landmarks. ## Disabled# * * * Breadcrumbs can be disabled using the `isDisabled` prop. This indicates that navigation is not currently available. When a breadcrumb is disabled, `onPress` will not be triggered, navigation will not occur, and links will be marked as `aria-disabled` for assistive technologies. <Breadcrumbs isDisabled> <Breadcrumb><Link href="/">Home</Link></Breadcrumb> <Breadcrumb><Link href="/react-aria/">React Aria</Link></Breadcrumb> <Breadcrumb><Link>Breadcrumbs</Link></Breadcrumb> </Breadcrumbs> <Breadcrumbs isDisabled> <Breadcrumb> <Link href="/">Home</Link> </Breadcrumb> <Breadcrumb> <Link href="/react-aria/">React Aria</Link> </Breadcrumb> <Breadcrumb> <Link>Breadcrumbs</Link> </Breadcrumb> </Breadcrumbs> <Breadcrumbs isDisabled > <Breadcrumb> <Link href="/"> Home </Link> </Breadcrumb> <Breadcrumb> <Link href="/react-aria/"> React Aria </Link> </Breadcrumb> <Breadcrumb> <Link> Breadcrumbs </Link> </Breadcrumb> </Breadcrumbs> 1. Home 2. React Aria 3. Breadcrumbs Show CSS .react-aria-Breadcrumbs { .react-aria-Link { &[data-disabled] { cursor: default; &:not([data-current]) { color: var(--text-color-disabled); } } } } .react-aria-Breadcrumbs { .react-aria-Link { &[data-disabled] { cursor: default; &:not([data-current]) { color: var(--text-color-disabled); } } } } .react-aria-Breadcrumbs { .react-aria-Link { &[data-disabled] { cursor: default; &:not([data-current]) { color: var(--text-color-disabled); } } } } Individual breadcrumbs can also be disabled by passing the `isDisabled` prop to the `<Link>` element: <Breadcrumbs> <Breadcrumb> <Link href="/">Home</Link> </Breadcrumb> <Breadcrumb> <Link isDisabled href="/react-aria/">React Aria</Link> </Breadcrumb> <Breadcrumb> <Link>Breadcrumbs</Link> </Breadcrumb> </Breadcrumbs> <Breadcrumbs> <Breadcrumb> <Link href="/">Home</Link> </Breadcrumb> <Breadcrumb> <Link isDisabled href="/react-aria/">React Aria</Link> </Breadcrumb> <Breadcrumb> <Link>Breadcrumbs</Link> </Breadcrumb> </Breadcrumbs> <Breadcrumbs> <Breadcrumb> <Link href="/"> Home </Link> </Breadcrumb> <Breadcrumb> <Link isDisabled href="/react-aria/" > React Aria </Link> </Breadcrumb> <Breadcrumb> <Link> Breadcrumbs </Link> </Breadcrumb> </Breadcrumbs> 1. Home 2. React Aria 3. Breadcrumbs ## Props# * * * ### Breadcrumbs# | Name | Type | Description | | --- | --- | --- | | `isDisabled` | `boolean` | Whether the breadcrumbs are disabled. | | `children` | `ReactNode | ( (item: T )) => ReactNode` | The contents of the collection. | | `dependencies` | `ReadonlyArray<any>` | Values that should invalidate the item cache when using dynamic collections. | | `items` | `Iterable<T>` | Item objects in the collection. | | `className` | `string` | The CSS className for the element. | | `style` | `CSSProperties` | The inline style for the element. | Events | Name | Type | Description | | --- | --- | --- | | `onAction` | `( (key: Key )) => void` | Handler that is called when a breadcrumb is clicked. | Layout | Name | Type | Description | | --- | --- | --- | | `slot` | `string | null` | A slot name for the component. Slots allow the component to receive props from a parent component. An explicit `null` value indicates that the local props completely override all props received from a parent. | Accessibility | Name | Type | Description | | --- | --- | --- | | `id` | `string` | The element's unique identifier. See MDN. | | `aria-label` | `string` | Defines a string value that labels the current element. | | `aria-labelledby` | `string` | Identifies the element (or elements) that labels the current element. | | `aria-describedby` | `string` | Identifies the element (or elements) that describes the object. | | `aria-details` | `string` | Identifies the element (or elements) that provide a detailed, extended description for the object. | ### Breadcrumb# | Name | Type | Description | | --- | --- | --- | | `id` | `Key` | A unique id for the breadcrumb, which will be passed to `onAction` when the breadcrumb is pressed. | | `children` | `ReactNode | ( (values: BreadcrumbRenderProps & & { defaultChildren: ReactNode | | undefined } )) => ReactNode` | The children of the component. A function may be provided to alter the children based on component state. | | `className` | `string | ( (values: BreadcrumbRenderProps & & { defaultClassName: string | | undefined } )) => string` | The CSS className for the element. A function may be provided to compute the class based on component state. | | `style` | `CSSProperties | ( (values: BreadcrumbRenderProps & & { defaultStyle: CSSProperties } )) => CSSProperties | undefined` | The inline style for the element. A function may be provided to compute the style based on component state. | ### Link# | Name | Type | Description | | --- | --- | --- | | `isDisabled` | `boolean` | Whether the link is disabled. | | `autoFocus` | `boolean` | Whether the element should receive focus on render. | | `href` | ` Href ` | A URL to link to. See MDN. | | `hrefLang` | `string` | Hints at the human language of the linked URL. SeeMDN. | | `target` | `HTMLAttributeAnchorTarget` | The target window for the link. See MDN. | | `rel` | `string` | The relationship between the linked resource and the current page. See MDN. | | `download` | `boolean | string` | Causes the browser to download the linked URL. A string may be provided to suggest a file name. See MDN. | | `ping` | `string` | A space-separated list of URLs to ping when the link is followed. See MDN. | | `referrerPolicy` | `HTMLAttributeReferrerPolicy` | How much of the referrer to send when following the link. See MDN. | | `routerOptions` | ` RouterOptions ` | Options for the configured client side router. | | `children` | `ReactNode | ( (values: LinkRenderProps & & { defaultChildren: ReactNode | | undefined } )) => ReactNode` | The children of the component. A function may be provided to alter the children based on component state. | | `className` | `string | ( (values: LinkRenderProps & & { defaultClassName: string | | undefined } )) => string` | The CSS className for the element. A function may be provided to compute the class based on component state. | | `style` | `CSSProperties | ( (values: LinkRenderProps & & { defaultStyle: CSSProperties } )) => CSSProperties | undefined` | The inline style for the element. A function may be provided to compute the style based on component state. | Events | Name | Type | Description | | --- | --- | --- | | `onPress` | `( (e: PressEvent )) => void` | Handler that is called when the press is released over the target. | | `onPressStart` | `( (e: PressEvent )) => void` | Handler that is called when a press interaction starts. | | `onPressEnd` | `( (e: PressEvent )) => void` | Handler that is called when a press interaction ends, either over the target or when the pointer leaves the target. | | `onPressChange` | `( (isPressed: boolean )) => void` | Handler that is called when the press state changes. | | `onPressUp` | `( (e: PressEvent )) => void` | Handler that is called when a press is released over the target, regardless of whether it started on the target or not. | | `onClick` | `( (e: MouseEvent<FocusableElement> )) => void` | **Not recommended – use `onPress` instead.** `onClick` is an alias for `onPress` provided for compatibility with other libraries. `onPress` provides additional event details for non-mouse interactions. | | `onFocus` | `( (e: FocusEvent<Target> )) => void` | Handler that is called when the element receives focus. | | `onBlur` | `( (e: FocusEvent<Target> )) => void` | Handler that is called when the element loses focus. | | `onFocusChange` | `( (isFocused: boolean )) => void` | Handler that is called when the element's focus status changes. | | `onKeyDown` | `( (e: KeyboardEvent )) => void` | Handler that is called when a key is pressed. | | `onKeyUp` | `( (e: KeyboardEvent )) => void` | Handler that is called when a key is released. | | `onHoverStart` | `( (e: HoverEvent )) => void` | Handler that is called when a hover interaction starts. | | `onHoverEnd` | `( (e: HoverEvent )) => void` | Handler that is called when a hover interaction ends. | | `onHoverChange` | `( (isHovering: boolean )) => void` | Handler that is called when the hover state changes. | Layout | Name | Type | Description | | --- | --- | --- | | `slot` | `string | null` | A slot name for the component. Slots allow the component to receive props from a parent component. An explicit `null` value indicates that the local props completely override all props received from a parent. | Accessibility | Name | Type | Description | | --- | --- | --- | | `aria-label` | `string` | Defines a string value that labels the current element. | | `aria-labelledby` | `string` | Identifies the element (or elements) that labels the current element. | | `aria-describedby` | `string` | Identifies the element (or elements) that describes the object. | | `aria-details` | `string` | Identifies the element (or elements) that provide a detailed, extended description for the object. | ## Styling# * * * React Aria components can be styled in many ways, including using CSS classes, inline styles, utility classes (e.g. Tailwind), CSS-in-JS (e.g. Styled Components), etc. By default, all components include a builtin `className` attribute which can be targeted using CSS selectors. These follow the `react-aria-ComponentName` naming convention. .react-aria-Breadcrumbs { /* ... */ } .react-aria-Breadcrumbs { /* ... */ } .react-aria-Breadcrumbs { /* ... */ } A custom `className` can also be specified on any component. This overrides the default `className` provided by React Aria with your own. <Breadcrumbs className="my-breadcrumbs"> {/* ... */} </Breadcrumbs> <Breadcrumbs className="my-breadcrumbs"> {/* ... */} </Breadcrumbs> <Breadcrumbs className="my-breadcrumbs"> {/* ... */} </Breadcrumbs> In addition, some components support multiple UI states (e.g. focused, placeholder, readonly, etc.). React Aria components expose states using data attributes, which you can target in CSS selectors. For example: .react-aria-Link[data-current] { /* ... */ } .react-aria-Link[data-current] { /* ... */ } .react-aria-Link[data-current] { /* ... */ } The `className` and `style` props also accept functions which receive states for styling. This lets you dynamically determine the classes or styles to apply, which is useful when using utility CSS libraries like Tailwind. <Link className={({ isCurrent }) => isCurrent ? 'bg-gray-700' : 'bg-gray-600'} /> <Link className={({ isCurrent }) => isCurrent ? 'bg-gray-700' : 'bg-gray-600'} /> <Link className={( { isCurrent } ) => isCurrent ? 'bg-gray-700' : 'bg-gray-600'} /> 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 link for all the breadcrumb items except the current one. <Breadcrumbs items={items}> {(item) => ( <Breadcrumb> {({ isCurrent }) => isCurrent ? <strong>{item.label}</strong> : <Link>{item.label}</Link>} </Breadcrumb> )} </Breadcrumbs> <Breadcrumbs items={items}> {(item) => ( <Breadcrumb> {({ isCurrent }) => isCurrent ? <strong>{item.label}</strong> : <Link>{item.label}</Link>} </Breadcrumb> )} </Breadcrumbs> <Breadcrumbs items={items} > {(item) => ( <Breadcrumb> {( { isCurrent } ) => isCurrent ? ( <strong> {item .label} </strong> ) : ( <Link> {item .label} </Link> )} </Breadcrumb> )} </Breadcrumbs> The states, selectors, and render props for all components used in `Breadcrumbs` are documented below. ### Breadcrumbs# `Breadcrumbs` can be targed with the `.react-aria-Breadcrumbs` CSS selector, or by overriding with a custom `className`. It is rendered as an `<ol>` element representing the list of items. ### Breadcrumb# A `Breadcrumb` can be targeted with the `.react-aria-Breadcrumb` CSS selector, or by overriding with a custom `className`. It is rendered as an `<li>` element, and should contain a `<Link>`. It may also include another element such as a separator icon. Breadcrumb support the following states and render props: | Name | CSS Selector | Description | | --- | --- | --- | | `isCurrent` | `[data-current]` | Whether the breadcrumb is for the current page. | | `isDisabled` | `[data-disabled]` | Whether the breadcrumb is disabled. | ### Link# A `Link` can be targeted with the `.react-aria-Link` CSS selector, or by overriding with a custom `className`. It is rendered as an `<a>` element when a href is provided via props. If only text is provided, it is rendered as a `<span>`. Links support the following states and render props: | Name | CSS Selector | Description | | --- | --- | --- | | `isCurrent` | `[data-current]` | Whether the link is the current item within a list. | | `isHovered` | `[data-hovered]` | Whether the link is currently hovered with a mouse. | | `isPressed` | `[data-pressed]` | Whether the link is currently in a pressed state. | | `isFocused` | `[data-focused]` | Whether the link is focused, either via a mouse or keyboard. | | `isFocusVisible` | `[data-focus-visible]` | Whether the link is keyboard focused. | | `isDisabled` | `[data-disabled]` | Whether the link is disabled. | ## Advanced customization# * * * ### Contexts# All React Aria Components export a corresponding context that can be used to send props to them from a parent element. This enables you to build your own compositional APIs similar to those found in React Aria Components itself. You can send any prop or ref via context that you could pass to the corresponding component. The local props and ref on the component are merged with the ones passed via context, with the local props taking precedence (following the rules documented in mergeProps). | Component | Context | Props | Ref | | --- | --- | --- | --- | | `Breadcrumbs` | `BreadcrumbsContext` | ` BreadcrumbsProps ` | `HTMLOListElement` | This example shows a `Router` component that accepts `Breadcrumbs` and `Link` elements as children, and tracks a history stack. When a link is clicked, it is pushed to the stack and automatically updates the breadcrumbs. When a breadcrumb is clicked, the stack is popped to that location. import type {PressEvent} from 'react-aria-components'; import {BreadcrumbsContext, LinkContext} from 'react-aria-components'; interface RouterItem { id: number, label: string } function Router({children}) { let [items, setItems] = React.useState<RouterItem[]>([ {id: 0, label: 'Home'}, {id: 1, label: 'React Aria'}, ]); // Pop stack when a breadcrumb item is clicked. let onAction = (id: Key) => { let i = items.findIndex(item => item.id === id); setItems(items.slice(0, i + 1)); }; // Push stack when a link is clicked. let onPress = (e: PressEvent) => { let label = e.target.textContent; setItems(items.concat({id: items.length, label})); }; return ( <BreadcrumbsContext.Provider value={{items, onAction}}> <LinkContext.Provider value={{onPress}}> {children} </LinkContext.Provider> </BreadcrumbsContext.Provider> ); } import type {PressEvent} from 'react-aria-components'; import { BreadcrumbsContext, LinkContext } from 'react-aria-components'; interface RouterItem { id: number; label: string; } function Router({ children }) { let [items, setItems] = React.useState<RouterItem[]>([ { id: 0, label: 'Home' }, { id: 1, label: 'React Aria' } ]); // Pop stack when a breadcrumb item is clicked. let onAction = (id: Key) => { let i = items.findIndex((item) => item.id === id); setItems(items.slice(0, i + 1)); }; // Push stack when a link is clicked. let onPress = (e: PressEvent) => { let label = e.target.textContent; setItems(items.concat({ id: items.length, label })); }; return ( <BreadcrumbsContext.Provider value={{ items, onAction }} > <LinkContext.Provider value={{ onPress }}> {children} </LinkContext.Provider> </BreadcrumbsContext.Provider> ); } import type {PressEvent} from 'react-aria-components'; import { BreadcrumbsContext, LinkContext } from 'react-aria-components'; interface RouterItem { id: number; label: string; } function Router( { children } ) { let [items, setItems] = React.useState< RouterItem[] >([ { id: 0, label: 'Home' }, { id: 1, label: 'React Aria' } ]); // Pop stack when a breadcrumb item is clicked. let onAction = ( id: Key ) => { let i = items .findIndex( (item) => item.id === id ); setItems( items.slice( 0, i + 1 ) ); }; // Push stack when a link is clicked. let onPress = ( e: PressEvent ) => { let label = e.target .textContent; setItems( items.concat({ id: items.length, label }) ); }; return ( <BreadcrumbsContext.Provider value={{ items, onAction }} > <LinkContext.Provider value={{ onPress }} > {children} </LinkContext.Provider> </BreadcrumbsContext.Provider> ); } **Note**: `LinkContext` only affects links outside `Breadcrumbs` because `Breadcrumbs` also provides a value for `LinkContext` which overrides an outer provider. See custom children below for more details. Now when you place `Breadcrumbs` inside a `Router`, it automatically has access to the location history via context. <Router> <Breadcrumbs> {(item: RouterItem) => <Breadcrumb><Link>{item.label}</Link></Breadcrumb>} </Breadcrumbs> <ul> <li><Link>Breadcrumbs</Link></li> <li><Link>Button</Link></li> <li><Link>Calendar</Link></li> </ul> </Router> <Router> <Breadcrumbs> {(item: RouterItem) => ( <Breadcrumb> <Link>{item.label}</Link> </Breadcrumb> )} </Breadcrumbs> <ul> <li> <Link>Breadcrumbs</Link> </li> <li> <Link>Button</Link> </li> <li> <Link>Calendar</Link> </li> </ul> </Router> <Router> <Breadcrumbs> {( item: RouterItem ) => ( <Breadcrumb> <Link> {item.label} </Link> </Breadcrumb> )} </Breadcrumbs> <ul> <li> <Link> Breadcrumbs </Link> </li> <li> <Link> Button </Link> </li> <li> <Link> Calendar </Link> </li> </ul> </Router> 1. Home 2. React Aria * Breadcrumbs * Button * Calendar ### Custom children# Breadcrumbs passes props to its child components, such as the links, via their associated contexts. These contexts are exported so you can also consume them in your own custom components. This enables you to reuse existing components from your app or component library together with React Aria Components. | Component | Context | Props | Ref | | --- | --- | --- | --- | | `Link` | `LinkContext` | ` LinkProps ` | `HTMLAnchorElement` | This example consumes from `LinkContext` in an existing styled link component to make it compatible with React Aria Components. The `useContextProps` hook merges the local props and ref with the ones provided via context by Breadcrumbs. useLink returns DOM props to spread onto the link element. import type {LinkProps} from 'react-aria-components'; import {LinkContext, useContextProps} from 'react-aria-components'; const MyCustomLink = React.forwardRef( (props: LinkProps, ref: React.ForwardedRef<HTMLAnchorElement>) => { // Merge the local props and ref with the ones provided via context. [props, ref] = useContextProps(props, ref, LinkContext); // ... your existing Link component let { linkProps } = useLink(props, ref); return <a {...linkProps} ref={ref} />; } ); import type {LinkProps} from 'react-aria-components'; import { LinkContext, useContextProps } from 'react-aria-components'; const MyCustomLink = React.forwardRef( ( props: LinkProps, ref: React.ForwardedRef<HTMLAnchorElement> ) => { // Merge the local props and ref with the ones provided via context. [props, ref] = useContextProps(props, ref, LinkContext); // ... your existing Link component let { linkProps } = useLink(props, ref); return <a {...linkProps} ref={ref} />; } ); import type {LinkProps} from 'react-aria-components'; import { LinkContext, useContextProps } from 'react-aria-components'; const MyCustomLink = React.forwardRef( ( props: LinkProps, ref: React.ForwardedRef< HTMLAnchorElement > ) => { // Merge the local props and ref with the ones provided via context. [props, ref] = useContextProps( props, ref, LinkContext ); // ... your existing Link component let { linkProps } = useLink( props, ref ); return ( <a {...linkProps} ref={ref} /> ); } ); Now you can use `MyCustomLink` within `Breadcrumbs`, in place of the builtin React Aria Components `Link`. <Breadcrumbs> <Breadcrumb><MyCustomLink>Custom link</MyCustomLink></Breadcrumb> {/* ... */} </Breadcrumbs> <Breadcrumbs> <Breadcrumb> <MyCustomLink>Custom link</MyCustomLink> </Breadcrumb> {/* ... */} </Breadcrumbs> <Breadcrumbs> <Breadcrumb> <MyCustomLink> Custom link </MyCustomLink> </Breadcrumb> {/* ... */} </Breadcrumbs> ### Hooks# If you need to customize things further, such as customizing the DOM structure, you can drop down to the lower level Hook-based API. See useBreadcrumbs for more details. --- ## Page: https://react-spectrum.adobe.com/react-aria/Disclosure.html # Disclosure A disclosure is a collapsible section of content. It is composed of a a header with a heading and trigger button, and a panel that contains the content. <table><tbody><tr><th>install</th><td><code>yarn add react-aria-components</code></td></tr><tr><th>version</th><td>1.10.1</td></tr><tr><th>usage</th><td><code><span>import</span> {Disclosure} <span>from</span> <span>'react-aria-components'</span></code></td></tr></tbody></table> View ARIA pattern W3C View repository GitHub View package NPM ## Example# * * * import {Button, Disclosure, DisclosurePanel, Heading} from 'react-aria-components'; <Disclosure> <Heading> <Button slot="trigger"> <svg viewBox="0 0 24 24"> <path d="m8.25 4.5 7.5 7.5-7.5 7.5" /> </svg> System Requirements </Button> </Heading> <DisclosurePanel> <p>Details about system requirements here.</p> </DisclosurePanel> </Disclosure> import { Button, Disclosure, DisclosurePanel, Heading } from 'react-aria-components'; <Disclosure> <Heading> <Button slot="trigger"> <svg viewBox="0 0 24 24"> <path d="m8.25 4.5 7.5 7.5-7.5 7.5" /> </svg> System Requirements </Button> </Heading> <DisclosurePanel> <p>Details about system requirements here.</p> </DisclosurePanel> </Disclosure> import { Button, Disclosure, DisclosurePanel, Heading } from 'react-aria-components'; <Disclosure> <Heading> <Button slot="trigger"> <svg viewBox="0 0 24 24"> <path d="m8.25 4.5 7.5 7.5-7.5 7.5" /> </svg> System Requirements </Button> </Heading> <DisclosurePanel> <p> Details about system requirements here. </p> </DisclosurePanel> </Disclosure> ### System Requirements Details about system requirements here. Show CSS .react-aria-Disclosure { .react-aria-Button[slot=trigger] { background: none; border: none; box-shadow: none; font-weight: bold; font-size: 16px; display: flex; align-items: center; gap: 8px; svg { rotate: 0deg; transition: rotate 200ms; width: 12px; height: 12px; fill: none; stroke: currentColor; stroke-width: 3px; } } &[data-expanded] .react-aria-Button[slot=trigger] svg { rotate: 90deg; } } .react-aria-DisclosurePanel { margin-left: 32px; } .react-aria-Disclosure { .react-aria-Button[slot=trigger] { background: none; border: none; box-shadow: none; font-weight: bold; font-size: 16px; display: flex; align-items: center; gap: 8px; svg { rotate: 0deg; transition: rotate 200ms; width: 12px; height: 12px; fill: none; stroke: currentColor; stroke-width: 3px; } } &[data-expanded] .react-aria-Button[slot=trigger] svg { rotate: 90deg; } } .react-aria-DisclosurePanel { margin-left: 32px; } .react-aria-Disclosure { .react-aria-Button[slot=trigger] { background: none; border: none; box-shadow: none; font-weight: bold; font-size: 16px; display: flex; align-items: center; gap: 8px; svg { rotate: 0deg; transition: rotate 200ms; width: 12px; height: 12px; fill: none; stroke: currentColor; stroke-width: 3px; } } &[data-expanded] .react-aria-Button[slot=trigger] svg { rotate: 90deg; } } .react-aria-DisclosurePanel { margin-left: 32px; } ## Features# * * * Disclosures can be built with the <details> and <summary> HTML elements, but this can be difficult to style, especially when adding adjacent interactive elements, like buttons, to the disclosure's heading. `Disclosure` helps achieve accessible disclosures implemented with the correct ARIA pattern while making custom styling easy. * **Flexible** - Structured such that it can be used standalone or combined with other disclosures to form a `DisclosureGroup` * **Keyboard Interaction** - When focused, a disclosure's visibility can be toggled with either the Enter or Space key, and the appropriate ARIA attributes are automatically applied. * **Accessible** - Uses hidden="until-found" in supported browsers, enabling find-in-page search support and improved search engine visibility for collapsed content * **Styleable** - Keyboard focus, disabled and expanded states are provided for easy styling. These states only apply when interacting with an appropriate input device, unlike CSS pseudo classes. ## Anatomy# * * * A disclosure consists of a button and panel of content. The button contains the label representing content within the panel, and the panel is the section of content that is associated with the button which is either expanded or collapsed. import {Button, Disclosure, DisclosurePanel, Heading} from 'react-aria-components'; <Disclosure> <Heading> <Button /> </Heading> <DisclosurePanel /> </Disclosure> import { Button, Disclosure, DisclosurePanel, Heading } from 'react-aria-components'; <Disclosure> <Heading> <Button /> </Heading> <DisclosurePanel /> </Disclosure> import { Button, Disclosure, DisclosurePanel, Heading } from 'react-aria-components'; <Disclosure> <Heading> <Button /> </Heading> <DisclosurePanel /> </Disclosure> ## Reusable wrappers# * * * If you will use a Disclosure in multiple places in your app, you can wrap all of the pieces into a reusable component. This way, the DOM structure, styling code, and other logic are defined in a single place and reused everywhere to ensure consistency. This example wraps `Disclosure` and all of its children together into a single component. import type {DisclosureProps} from 'react-aria-components'; interface MyDisclosureProps extends Omit<DisclosureProps, 'children'> { title?: string, children?: React.ReactNode } function MyDisclosure({title, children, ...props}: MyDisclosureProps) { return ( <Disclosure {...props}> <Heading> <Button slot="trigger"> <svg viewBox="0 0 24 24"> <path d="m8.25 4.5 7.5 7.5-7.5 7.5" /> </svg> {title} </Button> </Heading> <DisclosurePanel> <p>{children}</p> </DisclosurePanel> </Disclosure> ) } <MyDisclosure title="Manage your account"> Details on managing your account </MyDisclosure> import type {DisclosureProps} from 'react-aria-components'; interface MyDisclosureProps extends Omit<DisclosureProps, 'children'> { title?: string; children?: React.ReactNode; } function MyDisclosure( { title, children, ...props }: MyDisclosureProps ) { return ( <Disclosure {...props}> <Heading> <Button slot="trigger"> <svg viewBox="0 0 24 24"> <path d="m8.25 4.5 7.5 7.5-7.5 7.5" /> </svg> {title} </Button> </Heading> <DisclosurePanel> <p>{children}</p> </DisclosurePanel> </Disclosure> ); } <MyDisclosure title="Manage your account"> Details on managing your account </MyDisclosure> import type {DisclosureProps} from 'react-aria-components'; interface MyDisclosureProps extends Omit< DisclosureProps, 'children' > { title?: string; children?: React.ReactNode; } function MyDisclosure( { title, children, ...props }: MyDisclosureProps ) { return ( <Disclosure {...props} > <Heading> <Button slot="trigger"> <svg viewBox="0 0 24 24"> <path d="m8.25 4.5 7.5 7.5-7.5 7.5" /> </svg> {title} </Button> </Heading> <DisclosurePanel> <p>{children}</p> </DisclosurePanel> </Disclosure> ); } <MyDisclosure title="Manage your account"> Details on managing your account </MyDisclosure> ### Manage your account Details on managing your account ## Expanded# * * * An uncontrolled Disclosure can be expanded by default using the `defaultExpanded` prop. <MyDisclosure title="Download, Install, and Set Up" defaultExpanded> Instructions on how to download, install, and set up </MyDisclosure> <MyDisclosure title="Download, Install, and Set Up" defaultExpanded > Instructions on how to download, install, and set up </MyDisclosure> <MyDisclosure title="Download, Install, and Set Up" defaultExpanded > Instructions on how to download, install, and set up </MyDisclosure> ### Download, Install, and Set Up Instructions on how to download, install, and set up A controlled Disclosure can be expanded and collapsed using `isExpanded` and `onExpandedChange` function ControlledExpanded() { let [isExpanded, setIsExpanded] = React.useState(true); return ( <MyDisclosure title="Download, Install, and Set Up" isExpanded={isExpanded} onExpandedChange={setIsExpanded} > Instructions on how to download, install, and set up </MyDisclosure> ); } <ControlledExpanded /> function ControlledExpanded() { let [isExpanded, setIsExpanded] = React.useState(true); return ( <MyDisclosure title="Download, Install, and Set Up" isExpanded={isExpanded} onExpandedChange={setIsExpanded} > Instructions on how to download, install, and set up </MyDisclosure> ); } <ControlledExpanded /> function ControlledExpanded() { let [ isExpanded, setIsExpanded ] = React.useState( true ); return ( <MyDisclosure title="Download, Install, and Set Up" isExpanded={isExpanded} onExpandedChange={setIsExpanded} > Instructions on how to download, install, and set up </MyDisclosure> ); } <ControlledExpanded /> ### Download, Install, and Set Up Instructions on how to download, install, and set up ## Disabled# * * * A Disclosure can be disabled using the `isDisabled` prop. <MyDisclosure title="Introduction to Knitting" isDisabled> Details about knitting here </MyDisclosure> <MyDisclosure title="Introduction to Knitting" isDisabled> Details about knitting here </MyDisclosure> <MyDisclosure title="Introduction to Knitting" isDisabled > Details about knitting here </MyDisclosure> ### Introduction to Knitting Details about knitting here ## Interactive elements# * * * In some use cases, you may want to add an interactive element, like a button, adjacent to the disclosure's heading. Please ensure that these elements are siblings of the Heading element and not children. <Disclosure> <div style={{display: 'flex', alignItems: 'center'}}> <Heading> <Button slot="trigger"> <svg viewBox="0 0 24 24"> <path d="m8.25 4.5 7.5 7.5-7.5 7.5" /> </svg> System Requirements </Button> </Heading> <Button>Click me</Button> </div> <DisclosurePanel> <p>Details about system requirements here.</p> </DisclosurePanel> </Disclosure> <Disclosure> <div style={{display: 'flex', alignItems: 'center'}}> <Heading> <Button slot="trigger"> <svg viewBox="0 0 24 24"> <path d="m8.25 4.5 7.5 7.5-7.5 7.5" /> </svg> System Requirements </Button> </Heading> <Button>Click me</Button> </div> <DisclosurePanel> <p>Details about system requirements here.</p> </DisclosurePanel> </Disclosure> <Disclosure> <div style={{ display: 'flex', alignItems: 'center' }} > <Heading> <Button slot="trigger"> <svg viewBox="0 0 24 24"> <path d="m8.25 4.5 7.5 7.5-7.5 7.5" /> </svg> System Requirements </Button> </Heading> <Button> Click me </Button> </div> <DisclosurePanel> <p> Details about system requirements here. </p> </DisclosurePanel> </Disclosure> ### System Requirements Click me Details about system requirements here. ## Props# * * * ### Disclosure# | Name | Type | Description | | --- | --- | --- | | `id` | `Key` | An id for the disclosure when used within a DisclosureGroup, matching the id used in `expandedKeys`. | | `isDisabled` | `boolean` | Whether the disclosure is disabled. | | `isExpanded` | `boolean` | Whether the disclosure is expanded (controlled). | | `defaultExpanded` | `boolean` | Whether the disclosure is expanded by default (uncontrolled). | | `children` | `ReactNode | ( (values: DisclosureRenderProps & & { defaultChildren: ReactNode | | undefined } )) => ReactNode` | The children of the component. A function may be provided to alter the children based on component state. | | `className` | `string | ( (values: DisclosureRenderProps & & { defaultClassName: string | | undefined } )) => string` | The CSS className for the element. A function may be provided to compute the class based on component state. | | `style` | `CSSProperties | ( (values: DisclosureRenderProps & & { defaultStyle: CSSProperties } )) => CSSProperties | undefined` | The inline style for the element. A function may be provided to compute the style based on component state. | Events | Name | Type | Description | | --- | --- | --- | | `onExpandedChange` | `( (isExpanded: boolean )) => void` | Handler that is called when the disclosure's expanded state changes. | Layout | Name | Type | Description | | --- | --- | --- | | `slot` | `string | null` | A slot name for the component. Slots allow the component to receive props from a parent component. An explicit `null` value indicates that the local props completely override all props received from a parent. | ### Button# Show props | Name | Type | Default | Description | | --- | --- | --- | --- | | `form` | `string` | — | The `<form>` element to associate the button with. The value of this attribute must be the id of a `<form>` in the same document. | | `formAction` | `string` | — | The URL that processes the information submitted by the button. Overrides the action attribute of the button's form owner. | | `formEncType` | `string` | — | Indicates how to encode the form data that is submitted. | | `formMethod` | `string` | — | Indicates the HTTP method used to submit the form. | | `formNoValidate` | `boolean` | — | Indicates that the form is not to be validated when it is submitted. | | `formTarget` | `string` | — | Overrides the target attribute of the button's form owner. | | `name` | `string` | — | Submitted as a pair with the button's value as part of the form data. | | `value` | `string` | — | The value associated with the button's name when it's submitted with the form data. | | `isPending` | `boolean` | — | Whether the button is in a pending state. This disables press and hover events while retaining focusability, and announces the pending state to screen readers. | | `isDisabled` | `boolean` | — | Whether the button is disabled. | | `autoFocus` | `boolean` | — | Whether the element should receive focus on render. | | `type` | `'button' | 'submit' | 'reset'` | `'button'` | The behavior of the button when used in an HTML form. | | `children` | `ReactNode | ( (values: ButtonRenderProps & & { defaultChildren: ReactNode | | undefined } )) => ReactNode` | — | The children of the component. A function may be provided to alter the children based on component state. | | `className` | `string | ( (values: ButtonRenderProps & & { defaultClassName: string | | undefined } )) => string` | — | The CSS className for the element. A function may be provided to compute the class based on component state. | | `style` | `CSSProperties | ( (values: ButtonRenderProps & & { defaultStyle: CSSProperties } )) => CSSProperties | undefined` | — | The inline style for the element. A function may be provided to compute the style based on component state. | Events | Name | Type | Description | | --- | --- | --- | | `onPress` | `( (e: PressEvent )) => void` | Handler that is called when the press is released over the target. | | `onPressStart` | `( (e: PressEvent )) => void` | Handler that is called when a press interaction starts. | | `onPressEnd` | `( (e: PressEvent )) => void` | Handler that is called when a press interaction ends, either over the target or when the pointer leaves the target. | | `onPressChange` | `( (isPressed: boolean )) => void` | Handler that is called when the press state changes. | | `onPressUp` | `( (e: PressEvent )) => void` | Handler that is called when a press is released over the target, regardless of whether it started on the target or not. | | `onClick` | `( (e: MouseEvent<FocusableElement> )) => void` | **Not recommended – use `onPress` instead.** `onClick` is an alias for `onPress` provided for compatibility with other libraries. `onPress` provides additional event details for non-mouse interactions. | | `onFocus` | `( (e: FocusEvent<Target> )) => void` | Handler that is called when the element receives focus. | | `onBlur` | `( (e: FocusEvent<Target> )) => void` | Handler that is called when the element loses focus. | | `onFocusChange` | `( (isFocused: boolean )) => void` | Handler that is called when the element's focus status changes. | | `onKeyDown` | `( (e: KeyboardEvent )) => void` | Handler that is called when a key is pressed. | | `onKeyUp` | `( (e: KeyboardEvent )) => void` | Handler that is called when a key is released. | | `onHoverStart` | `( (e: HoverEvent )) => void` | Handler that is called when a hover interaction starts. | | `onHoverEnd` | `( (e: HoverEvent )) => void` | Handler that is called when a hover interaction ends. | | `onHoverChange` | `( (isHovering: boolean )) => void` | Handler that is called when the hover state changes. | Layout | Name | Type | Description | | --- | --- | --- | | `slot` | `string | null` | A slot name for the component. Slots allow the component to receive props from a parent component. An explicit `null` value indicates that the local props completely override all props received from a parent. | Accessibility | Name | Type | Description | | --- | --- | --- | | `id` | `string` | The element's unique identifier. See MDN. | | `excludeFromTabOrder` | `boolean` | Whether to exclude the element from the sequential tab order. If true, the element will not be focusable via the keyboard by tabbing. This should be avoided except in rare scenarios where an alternative means of accessing the element or its functionality via the keyboard is available. | | `preventFocusOnPress` | `boolean` | Whether to prevent focus from moving to the button when pressing it. Caution, this can make the button inaccessible and should only be used when alternative keyboard interaction is provided, such as ComboBox's MenuTrigger or a NumberField's increment/decrement control. | | `aria-expanded` | `boolean | 'true' | 'false'` | Indicates whether the element, or another grouping element it controls, is currently expanded or collapsed. | | `aria-haspopup` | `boolean | 'menu' | 'listbox' | 'tree' | 'grid' | 'dialog' | 'true' | 'false'` | Indicates the availability and type of interactive popup element, such as menu or dialog, that can be triggered by an element. | | `aria-controls` | `string` | Identifies the element (or elements) whose contents or presence are controlled by the current element. | | `aria-pressed` | `boolean | 'true' | 'false' | 'mixed'` | Indicates the current "pressed" state of toggle buttons. | | `aria-current` | `boolean | 'true' | 'false' | 'page' | 'step' | 'location' | 'date' | 'time'` | Indicates whether this element represents the current item within a container or set of related elements. | | `aria-label` | `string` | Defines a string value that labels the current element. | | `aria-labelledby` | `string` | Identifies the element (or elements) that labels the current element. | | `aria-describedby` | `string` | Identifies the element (or elements) that describes the object. | | `aria-details` | `string` | Identifies the element (or elements) that provide a detailed, extended description for the object. | ### DisclosurePanel# Show props | Name | Type | Description | | --- | --- | --- | | `children` | `ReactNode` | The children of the component. | | `className` | `string | ( (values: DisclosurePanelRenderProps & & { defaultClassName: string | | undefined } )) => string` | The CSS className for the element. A function may be provided to compute the class based on component state. | | `style` | `CSSProperties | ( (values: DisclosurePanelRenderProps & & { defaultStyle: CSSProperties } )) => CSSProperties | undefined` | The inline style for the element. A function may be provided to compute the style based on component state. | Accessibility | Name | Type | Default | Description | | --- | --- | --- | --- | | `role` | `'group' | 'region'` | `'group'` | The accessibility role for the disclosure's panel. | | `id` | `string` | — | The element's unique identifier. See MDN. | ## Styling# * * * React Aria components can be styled in many ways, including using CSS classes, inline styles, utility classes (e.g. Tailwind), CSS-in-JS (e.g. Styled Components), etc. By default, all components include a builtin `className` attribute which can be targeted using CSS selectors. These follow the `react-aria-ComponentName` naming convention. .react-aria-Disclosure { /* ... */ } .react-aria-Disclosure { /* ... */ } .react-aria-Disclosure { /* ... */ } A custom `className` can also be specified on any component. This overrides the default `className` provided by React Aria with your own. <Disclosure className="my-disclosure"> {/* ... */} </Disclosure> <Disclosure className="my-disclosure"> {/* ... */} </Disclosure> <Disclosure className="my-disclosure"> {/* ... */} </Disclosure> In addition, some components support multiple UI states (e.g. focused, placeholder, readonly, etc.). React Aria components expose states using data attributes, which you can target in CSS selectors. For example: .react-aria-Disclosure[data-expanded] { /* ... */ } .react-aria-Disclosure[data-expanded] { /* ... */ } .react-aria-Disclosure[data-expanded] { /* ... */ } The `className` and `style` props also accept functions which receive states for styling. This lets you dynamically determine the classes or styles to apply, which is useful when using utility CSS libraries like Tailwind. <Disclosure className={({ isExpanded }) => isExpanded ? 'border-blue-500' : 'border-gray-600'} /> <Disclosure className={({ isExpanded }) => isExpanded ? 'border-blue-500' : 'border-gray-600'} /> <Disclosure className={( { isExpanded } ) => isExpanded ? 'border-blue-500' : 'border-gray-600'} /> The states, selectors, and render props for each component used in a `Disclosure` are documented below. ### Disclosure# A `Disclosure` can be targeted with the `.react-aria-Disclosure` CSS selector, or by overriding with a custom `className`. It supports the following states: | Name | CSS Selector | Description | | --- | --- | --- | | `isExpanded` | `[data-expanded]` | Whether the disclosure is expanded. | | `isFocusVisibleWithin` | `[data-focus-visible-within]` | Whether the disclosure has keyboard focus. | | `isDisabled` | `[data-disabled]` | Whether the disclosure is disabled. | | `state` | `—` | State of the disclosure. | ### Button# A `Button` can be targeted with the `.react-aria-Button` CSS selector, or by overriding with a custom `className`. It supports the following states: | Name | CSS Selector | Description | | --- | --- | --- | | `isHovered` | `[data-hovered]` | Whether the button is currently hovered with a mouse. | | `isPressed` | `[data-pressed]` | Whether the button is currently in a pressed state. | | `isFocused` | `[data-focused]` | Whether the button is focused, either via a mouse or keyboard. | | `isFocusVisible` | `[data-focus-visible]` | Whether the button is keyboard focused. | | `isDisabled` | `[data-disabled]` | Whether the button is disabled. | | `isPending` | `[data-pending]` | Whether the button is currently in a pending state. | ### DisclosurePanel# A `DisclosurePanel` can be targeted with the `.react-aria-DisclosurePanel` CSS selector, or by overriding with a custom `className`. | Name | CSS Selector | Description | | --- | --- | --- | | `isFocusVisibleWithin` | `[data-focus-visible-within]` | Whether keyboard focus is within the disclosure panel. | ## Advanced Customization# * * * ### Contexts# All React Aria Components export a corresponding context that can be used to send props to them from a parent element. This enables you to build your own compositional APIs similar to those found in React Aria Components itself. You can send any prop or ref via context that you could pass to the corresponding component. The local props and ref on the component are merged with the ones passed via context, with the local props taking precedence (following the rules documented in mergeProps). | Component | Context | Props | Ref | | --- | --- | --- | --- | | `Disclosure` | `DisclosureContext` | ` DisclosureProps ` | `HTMLDivElement` | This example shows a `DisclosureGroup` component that renders a group of disclosures. The entire group can be marked as disabled via the `isDisabled` prop, which is passed to all child disclosures via the `DisclosureContext` provider. import {DisclosureContext} from 'react-aria-components'; interface DisclosureGroupProps { children?: React.ReactNode, isDisabled?: boolean } function DisclosureGroup({children, isDisabled}: DisclosureGroupProps) { return ( <div style={{display: 'flex', flexDirection: 'column'}}> <DisclosureContext.Provider value={{isDisabled}}> {children} </DisclosureContext.Provider> </div> ) } <DisclosureGroup isDisabled> <MyDisclosure title="How to make a return" > Details about returning items </MyDisclosure> <MyDisclosure title="How to cancel an order" > Details on canceling an order </MyDisclosure> </DisclosureGroup> import {DisclosureContext} from 'react-aria-components'; interface DisclosureGroupProps { children?: React.ReactNode; isDisabled?: boolean; } function DisclosureGroup( { children, isDisabled }: DisclosureGroupProps ) { return ( <div style={{ display: 'flex', flexDirection: 'column' }} > <DisclosureContext.Provider value={{ isDisabled }}> {children} </DisclosureContext.Provider> </div> ); } <DisclosureGroup isDisabled> <MyDisclosure title="How to make a return"> Details about returning items </MyDisclosure> <MyDisclosure title="How to cancel an order"> Details on canceling an order </MyDisclosure> </DisclosureGroup> import {DisclosureContext} from 'react-aria-components'; interface DisclosureGroupProps { children?: React.ReactNode; isDisabled?: boolean; } function DisclosureGroup( { children, isDisabled }: DisclosureGroupProps ) { return ( <div style={{ display: 'flex', flexDirection: 'column' }} > <DisclosureContext.Provider value={{ isDisabled }} > {children} </DisclosureContext.Provider> </div> ); } <DisclosureGroup isDisabled > <MyDisclosure title="How to make a return"> Details about returning items </MyDisclosure> <MyDisclosure title="How to cancel an order"> Details on canceling an order </MyDisclosure> </DisclosureGroup> ### How to make a return Details about returning items ### How to cancel an order Details on canceling an order ### State# DisclosureGroup provides a `DisclosureState` object to its children via `DisclosureStateContext`. This can be used to access and manipulate the disclosure group's state. ### Hooks# If you need to customize things further, such as accessing internal state or customizing DOM structure, you can drop down to the lower level Hook-based API. See useDisclosure. --- ## Page: https://react-spectrum.adobe.com/react-aria/DisclosureGroup.html # DisclosureGroup A DisclosureGroup is a grouping of related disclosures, sometimes called an accordion. It supports both single and multiple expanded items. <table><tbody><tr><th>install</th><td><code>yarn add react-aria-components</code></td></tr><tr><th>version</th><td>1.10.1</td></tr><tr><th>usage</th><td><code><span>import</span> {DisclosureGroup} <span>from</span> <span>'react-aria-components'</span></code></td></tr></tbody></table> View ARIA pattern W3C View repository GitHub View package NPM ## Example# * * * import {Button, Disclosure, DisclosureGroup, DisclosurePanel, Heading} from 'react-aria-components'; <DisclosureGroup defaultExpandedKeys={['personal']}> <Disclosure id="personal"> <Heading> <Button slot="trigger"> <svg viewBox="0 0 24 24"> <path d="m8.25 4.5 7.5 7.5-7.5 7.5" /> </svg> Personal Information </Button> </Heading> <DisclosurePanel> <p>Personal information form here.</p> </DisclosurePanel> </Disclosure> <Disclosure id="billing"> <Heading> <Button slot="trigger"> <svg viewBox="0 0 24 24"> <path d="m8.25 4.5 7.5 7.5-7.5 7.5" /> </svg> Billing Address </Button> </Heading> <DisclosurePanel> <p>Billing address form here.</p> </DisclosurePanel> </Disclosure> </DisclosureGroup> import { Button, Disclosure, DisclosureGroup, DisclosurePanel, Heading } from 'react-aria-components'; <DisclosureGroup defaultExpandedKeys={['personal']}> <Disclosure id="personal"> <Heading> <Button slot="trigger"> <svg viewBox="0 0 24 24"> <path d="m8.25 4.5 7.5 7.5-7.5 7.5" /> </svg> Personal Information </Button> </Heading> <DisclosurePanel> <p>Personal information form here.</p> </DisclosurePanel> </Disclosure> <Disclosure id="billing"> <Heading> <Button slot="trigger"> <svg viewBox="0 0 24 24"> <path d="m8.25 4.5 7.5 7.5-7.5 7.5" /> </svg> Billing Address </Button> </Heading> <DisclosurePanel> <p>Billing address form here.</p> </DisclosurePanel> </Disclosure> </DisclosureGroup> import { Button, Disclosure, DisclosureGroup, DisclosurePanel, Heading } from 'react-aria-components'; <DisclosureGroup defaultExpandedKeys={[ 'personal' ]} > <Disclosure id="personal"> <Heading> <Button slot="trigger"> <svg viewBox="0 0 24 24"> <path d="m8.25 4.5 7.5 7.5-7.5 7.5" /> </svg> Personal Information </Button> </Heading> <DisclosurePanel> <p> Personal information form here. </p> </DisclosurePanel> </Disclosure> <Disclosure id="billing"> <Heading> <Button slot="trigger"> <svg viewBox="0 0 24 24"> <path d="m8.25 4.5 7.5 7.5-7.5 7.5" /> </svg> Billing Address </Button> </Heading> <DisclosurePanel> <p> Billing address form here. </p> </DisclosurePanel> </Disclosure> </DisclosureGroup> ### Personal Information Personal information form here. ### Billing Address Billing address form here. ## Features# * * * Disclosure groups can be built by combining multiple disclosures built in HTML with the <details> and <summary> HTML elements, but this can be difficult to style, especially when adding adjacent interactive elements, like buttons, to the disclosure's heading. `DisclosureGroup` helps achieve accessible disclosures implemented with the correct ARIA pattern while making custom styling easy. * **Accessible** - Disclosure group is exposed to assistive technology via ARIA. Uses hidden="until-found" in supported browsers, enabling find-in-page search support and improved search engine visibility for collapsed content. * **Styleable** - Disclosures include builtin states for styling such as keyboard focus, disabled, and expanded states. ## Anatomy# * * * A disclosure group consists of a set of disclosures. Each disclosure includes a button within a heading and panel of content that is either shown or hidden. Zero or more disclosures within a group can be expanded at the same time, however, by default, only one disclosure can be expanded at a time. Users may click or touch a disclosure to expand it, or use the Tab key to navigate between disclosures and the Enter or Space key to toggle it. import {Button, Disclosure, DisclosureGroup, DisclosurePanel, Heading} from 'react-aria-components'; <DisclosureGroup> <Disclosure> <Heading> <Button /> </Heading> <DisclosurePanel /> </Disclosure> </DisclosureGroup> import { Button, Disclosure, DisclosureGroup, DisclosurePanel, Heading } from 'react-aria-components'; <DisclosureGroup> <Disclosure> <Heading> <Button /> </Heading> <DisclosurePanel /> </Disclosure> </DisclosureGroup> import { Button, Disclosure, DisclosureGroup, DisclosurePanel, Heading } from 'react-aria-components'; <DisclosureGroup> <Disclosure> <Heading> <Button /> </Heading> <DisclosurePanel /> </Disclosure> </DisclosureGroup> ### Composed Components# Disclosure A disclosure is a collapsible section of content. ## Expanded# * * * ### Default expanded# An uncontrolled DisclosureGroup can be expanded by default using the `defaultExpandedKeys` prop. The `defaultExpandedKeys` must match the `id` on the disclosure(s) component that you wish to expand. import type {DisclosureProps} from 'react-aria-components'; interface MyDisclosureProps extends Omit<DisclosureProps, 'children'> { title?: string, children?: React.ReactNode } function MyDisclosure({title, children, ...props}: MyDisclosureProps) { return ( <Disclosure {...props}> <Heading> <Button slot="trigger"> <svg viewBox="0 0 24 24"> <path d="m8.25 4.5 7.5 7.5-7.5 7.5" /> </svg> {title} </Button> </Heading> <DisclosurePanel> <p>{children}</p> </DisclosurePanel> </Disclosure> ) } <DisclosureGroup defaultExpandedKeys={["system"]}> <MyDisclosure id="system" title="System Requirements" > Details about system requirements here </MyDisclosure> <MyDisclosure id="personal" title="Personal Information" > Details about personal information here </MyDisclosure> </DisclosureGroup> import type {DisclosureProps} from 'react-aria-components'; interface MyDisclosureProps extends Omit<DisclosureProps, 'children'> { title?: string; children?: React.ReactNode; } function MyDisclosure( { title, children, ...props }: MyDisclosureProps ) { return ( <Disclosure {...props}> <Heading> <Button slot="trigger"> <svg viewBox="0 0 24 24"> <path d="m8.25 4.5 7.5 7.5-7.5 7.5" /> </svg> {title} </Button> </Heading> <DisclosurePanel> <p>{children}</p> </DisclosurePanel> </Disclosure> ); } <DisclosureGroup defaultExpandedKeys={['system']}> <MyDisclosure id="system" title="System Requirements"> Details about system requirements here </MyDisclosure> <MyDisclosure id="personal" title="Personal Information" > Details about personal information here </MyDisclosure> </DisclosureGroup> import type {DisclosureProps} from 'react-aria-components'; interface MyDisclosureProps extends Omit< DisclosureProps, 'children' > { title?: string; children?: React.ReactNode; } function MyDisclosure( { title, children, ...props }: MyDisclosureProps ) { return ( <Disclosure {...props} > <Heading> <Button slot="trigger"> <svg viewBox="0 0 24 24"> <path d="m8.25 4.5 7.5 7.5-7.5 7.5" /> </svg> {title} </Button> </Heading> <DisclosurePanel> <p>{children}</p> </DisclosurePanel> </Disclosure> ); } <DisclosureGroup defaultExpandedKeys={[ 'system' ]} > <MyDisclosure id="system" title="System Requirements" > Details about system requirements here </MyDisclosure> <MyDisclosure id="personal" title="Personal Information" > Details about personal information here </MyDisclosure> </DisclosureGroup> ### System Requirements Details about system requirements here ### Personal Information Details about personal information here ### Controlled expanded# A controlled DisclosureGroup can be expanded and collapsed using `expandedKeys` and `onExpandedChange`. The `expandedKeys` must match the `id` on the disclosure component(s) that you wish to expand. import {Key} from '@react-types/shared'; function ControlledExpanded() { let [expandedKeys, setExpandedKeys] = React.useState<Set<Key>>( new Set(['system']) ); return ( <> <DisclosureGroup expandedKeys={expandedKeys} onExpandedChange={setExpandedKeys} > <MyDisclosure id="system" title="System Requirements"> Details about system requirements here </MyDisclosure> <MyDisclosure id="personal" title="Personal Information"> Details about personal information here </MyDisclosure> </DisclosureGroup> <div style={{ marginTop: '20px' }}>You have expanded: {expandedKeys}</div> </> ); } import {Key} from '@react-types/shared'; function ControlledExpanded() { let [expandedKeys, setExpandedKeys] = React.useState< Set<Key> >(new Set(['system'])); return ( <> <DisclosureGroup expandedKeys={expandedKeys} onExpandedChange={setExpandedKeys} > <MyDisclosure id="system" title="System Requirements" > Details about system requirements here </MyDisclosure> <MyDisclosure id="personal" title="Personal Information" > Details about personal information here </MyDisclosure> </DisclosureGroup> <div style={{ marginTop: '20px' }}> You have expanded: {expandedKeys} </div> </> ); } import {Key} from '@react-types/shared'; function ControlledExpanded() { let [ expandedKeys, setExpandedKeys ] = React.useState< Set<Key> >(new Set(['system'])); return ( <> <DisclosureGroup expandedKeys={expandedKeys} onExpandedChange={setExpandedKeys} > <MyDisclosure id="system" title="System Requirements" > Details about system requirements here </MyDisclosure> <MyDisclosure id="personal" title="Personal Information" > Details about personal information here </MyDisclosure> </DisclosureGroup> <div style={{ marginTop: '20px' }} > You have expanded:{' '} {expandedKeys} </div> </> ); } ### System Requirements Details about system requirements here ### Personal Information Details about personal information here You have expanded: system ### Multiple expanded# By default, only one disclosure will be open at a time. To allow multiple disclosures to expand, use the `allowsMultipleExpanded` prop. <DisclosureGroup defaultExpandedKeys={['system', 'personal']} allowsMultipleExpanded > <MyDisclosure id="system" title="System Requirements"> Details about system requirements here </MyDisclosure> <MyDisclosure id="personal" title="Personal Information"> Details about personal information here </MyDisclosure> </DisclosureGroup> <DisclosureGroup defaultExpandedKeys={['system', 'personal']} allowsMultipleExpanded > <MyDisclosure id="system" title="System Requirements"> Details about system requirements here </MyDisclosure> <MyDisclosure id="personal" title="Personal Information"> Details about personal information here </MyDisclosure> </DisclosureGroup> <DisclosureGroup defaultExpandedKeys={[ 'system', 'personal' ]} allowsMultipleExpanded > <MyDisclosure id="system" title="System Requirements" > Details about system requirements here </MyDisclosure> <MyDisclosure id="personal" title="Personal Information" > Details about personal information here </MyDisclosure> </DisclosureGroup> ### System Requirements Details about system requirements here ### Personal Information Details about personal information here ## Disabled# * * * A DisclosureGroup can be disabled using the `isDisabled` prop. <DisclosureGroup isDisabled> <MyDisclosure id="system" title="System Requirements" > Details about system requirements here </MyDisclosure> <MyDisclosure id="personal" title="Personal Information" > Details about personal information here" </MyDisclosure> </DisclosureGroup> <DisclosureGroup isDisabled> <MyDisclosure id="system" title="System Requirements" > Details about system requirements here </MyDisclosure> <MyDisclosure id="personal" title="Personal Information" > Details about personal information here" </MyDisclosure> </DisclosureGroup> <DisclosureGroup isDisabled > <MyDisclosure id="system" title="System Requirements" > Details about system requirements here </MyDisclosure> <MyDisclosure id="personal" title="Personal Information" > Details about personal information here" </MyDisclosure> </DisclosureGroup> ### System Requirements Details about system requirements here ### Personal Information Details about personal information here" ## Interactive elements# * * * In some use cases, you may want to add an interactive element, like a button, adjacent to the disclosure's heading. Please ensure that these elements are siblings of the Heading element and not children. <DisclosureGroup> <Disclosure id="system"> <div style={{display: 'flex', alignItems: 'center'}}> <Heading> <Button slot="trigger"> <svg viewBox="0 0 24 24"> <path d="m8.25 4.5 7.5 7.5-7.5 7.5" /> </svg> System Requirements </Button> </Heading> <Button>Click me</Button> </div> <DisclosurePanel> <p>Details about system requirements here.</p> </DisclosurePanel> </Disclosure> <Disclosure id="personal"> <div style={{display: 'flex', alignItems: 'center'}}> <Heading> <Button slot="trigger"> <svg viewBox="0 0 24 24"> <path d="m8.25 4.5 7.5 7.5-7.5 7.5" /> </svg> Personal Information </Button> </Heading> <Button>Click me</Button> </div> <DisclosurePanel> <p>Details about personal information here.</p> </DisclosurePanel> </Disclosure> </DisclosureGroup> <DisclosureGroup> <Disclosure id="system"> <div style={{display: 'flex', alignItems: 'center'}}> <Heading> <Button slot="trigger"> <svg viewBox="0 0 24 24"> <path d="m8.25 4.5 7.5 7.5-7.5 7.5" /> </svg> System Requirements </Button> </Heading> <Button>Click me</Button> </div> <DisclosurePanel> <p>Details about system requirements here.</p> </DisclosurePanel> </Disclosure> <Disclosure id="personal"> <div style={{display: 'flex', alignItems: 'center'}}> <Heading> <Button slot="trigger"> <svg viewBox="0 0 24 24"> <path d="m8.25 4.5 7.5 7.5-7.5 7.5" /> </svg> Personal Information </Button> </Heading> <Button>Click me</Button> </div> <DisclosurePanel> <p>Details about personal information here.</p> </DisclosurePanel> </Disclosure> </DisclosureGroup> <DisclosureGroup> <Disclosure id="system"> <div style={{ display: 'flex', alignItems: 'center' }} > <Heading> <Button slot="trigger"> <svg viewBox="0 0 24 24"> <path d="m8.25 4.5 7.5 7.5-7.5 7.5" /> </svg> System Requirements </Button> </Heading> <Button> Click me </Button> </div> <DisclosurePanel> <p> Details about system requirements here. </p> </DisclosurePanel> </Disclosure> <Disclosure id="personal"> <div style={{ display: 'flex', alignItems: 'center' }} > <Heading> <Button slot="trigger"> <svg viewBox="0 0 24 24"> <path d="m8.25 4.5 7.5 7.5-7.5 7.5" /> </svg> Personal Information </Button> </Heading> <Button> Click me </Button> </div> <DisclosurePanel> <p> Details about personal information here. </p> </DisclosurePanel> </Disclosure> </DisclosureGroup> ### System Requirements Click me Details about system requirements here. ### Personal Information Click me Details about personal information here. ## Props# * * * ### DisclosureGroup# | Name | Type | Description | | --- | --- | --- | | `allowsMultipleExpanded` | `boolean` | Whether multiple items can be expanded at the same time. | | `isDisabled` | `boolean` | Whether all items are disabled. | | `expandedKeys` | `Iterable<Key>` | The currently expanded keys in the group (controlled). | | `defaultExpandedKeys` | `Iterable<Key>` | The initial expanded keys in the group (uncontrolled). | | `children` | `ReactNode | ( (values: DisclosureGroupRenderProps & & { defaultChildren: ReactNode | | undefined } )) => ReactNode` | The children of the component. A function may be provided to alter the children based on component state. | | `className` | `string | ( (values: DisclosureGroupRenderProps & & { defaultClassName: string | | undefined } )) => string` | The CSS className for the element. A function may be provided to compute the class based on component state. | | `style` | `CSSProperties | ( (values: DisclosureGroupRenderProps & & { defaultStyle: CSSProperties } )) => CSSProperties | undefined` | The inline style for the element. A function may be provided to compute the style based on component state. | Events | Name | Type | Description | | --- | --- | --- | | `onExpandedChange` | `( (keys: Set<Key> )) => any` | Handler that is called when items are expanded or collapsed. | Accessibility | Name | Type | Description | | --- | --- | --- | | `id` | `string` | The element's unique identifier. See MDN. | ### Disclosure# Within a `<DisclosureGroup>`, most `<Disclosure>` props are set automatically. The `id` prop is required to identify the disclosure within the group. Show props | Name | Type | Description | | --- | --- | --- | | `id` | `Key` | An id for the disclosure when used within a DisclosureGroup, matching the id used in `expandedKeys`. | | `isDisabled` | `boolean` | Whether the disclosure is disabled. | | `isExpanded` | `boolean` | Whether the disclosure is expanded (controlled). | | `defaultExpanded` | `boolean` | Whether the disclosure is expanded by default (uncontrolled). | | `children` | `ReactNode | ( (values: DisclosureRenderProps & & { defaultChildren: ReactNode | | undefined } )) => ReactNode` | The children of the component. A function may be provided to alter the children based on component state. | | `className` | `string | ( (values: DisclosureRenderProps & & { defaultClassName: string | | undefined } )) => string` | The CSS className for the element. A function may be provided to compute the class based on component state. | | `style` | `CSSProperties | ( (values: DisclosureRenderProps & & { defaultStyle: CSSProperties } )) => CSSProperties | undefined` | The inline style for the element. A function may be provided to compute the style based on component state. | Events | Name | Type | Description | | --- | --- | --- | | `onExpandedChange` | `( (isExpanded: boolean )) => void` | Handler that is called when the disclosure's expanded state changes. | Layout | Name | Type | Description | | --- | --- | --- | | `slot` | `string | null` | A slot name for the component. Slots allow the component to receive props from a parent component. An explicit `null` value indicates that the local props completely override all props received from a parent. | ### Button# Show props | Name | Type | Default | Description | | --- | --- | --- | --- | | `form` | `string` | — | The `<form>` element to associate the button with. The value of this attribute must be the id of a `<form>` in the same document. | | `formAction` | `string` | — | The URL that processes the information submitted by the button. Overrides the action attribute of the button's form owner. | | `formEncType` | `string` | — | Indicates how to encode the form data that is submitted. | | `formMethod` | `string` | — | Indicates the HTTP method used to submit the form. | | `formNoValidate` | `boolean` | — | Indicates that the form is not to be validated when it is submitted. | | `formTarget` | `string` | — | Overrides the target attribute of the button's form owner. | | `name` | `string` | — | Submitted as a pair with the button's value as part of the form data. | | `value` | `string` | — | The value associated with the button's name when it's submitted with the form data. | | `isPending` | `boolean` | — | Whether the button is in a pending state. This disables press and hover events while retaining focusability, and announces the pending state to screen readers. | | `isDisabled` | `boolean` | — | Whether the button is disabled. | | `autoFocus` | `boolean` | — | Whether the element should receive focus on render. | | `type` | `'button' | 'submit' | 'reset'` | `'button'` | The behavior of the button when used in an HTML form. | | `children` | `ReactNode | ( (values: ButtonRenderProps & & { defaultChildren: ReactNode | | undefined } )) => ReactNode` | — | The children of the component. A function may be provided to alter the children based on component state. | | `className` | `string | ( (values: ButtonRenderProps & & { defaultClassName: string | | undefined } )) => string` | — | The CSS className for the element. A function may be provided to compute the class based on component state. | | `style` | `CSSProperties | ( (values: ButtonRenderProps & & { defaultStyle: CSSProperties } )) => CSSProperties | undefined` | — | The inline style for the element. A function may be provided to compute the style based on component state. | Events | Name | Type | Description | | --- | --- | --- | | `onPress` | `( (e: PressEvent )) => void` | Handler that is called when the press is released over the target. | | `onPressStart` | `( (e: PressEvent )) => void` | Handler that is called when a press interaction starts. | | `onPressEnd` | `( (e: PressEvent )) => void` | Handler that is called when a press interaction ends, either over the target or when the pointer leaves the target. | | `onPressChange` | `( (isPressed: boolean )) => void` | Handler that is called when the press state changes. | | `onPressUp` | `( (e: PressEvent )) => void` | Handler that is called when a press is released over the target, regardless of whether it started on the target or not. | | `onClick` | `( (e: MouseEvent<FocusableElement> )) => void` | **Not recommended – use `onPress` instead.** `onClick` is an alias for `onPress` provided for compatibility with other libraries. `onPress` provides additional event details for non-mouse interactions. | | `onFocus` | `( (e: FocusEvent<Target> )) => void` | Handler that is called when the element receives focus. | | `onBlur` | `( (e: FocusEvent<Target> )) => void` | Handler that is called when the element loses focus. | | `onFocusChange` | `( (isFocused: boolean )) => void` | Handler that is called when the element's focus status changes. | | `onKeyDown` | `( (e: KeyboardEvent )) => void` | Handler that is called when a key is pressed. | | `onKeyUp` | `( (e: KeyboardEvent )) => void` | Handler that is called when a key is released. | | `onHoverStart` | `( (e: HoverEvent )) => void` | Handler that is called when a hover interaction starts. | | `onHoverEnd` | `( (e: HoverEvent )) => void` | Handler that is called when a hover interaction ends. | | `onHoverChange` | `( (isHovering: boolean )) => void` | Handler that is called when the hover state changes. | Layout | Name | Type | Description | | --- | --- | --- | | `slot` | `string | null` | A slot name for the component. Slots allow the component to receive props from a parent component. An explicit `null` value indicates that the local props completely override all props received from a parent. | Accessibility | Name | Type | Description | | --- | --- | --- | | `id` | `string` | The element's unique identifier. See MDN. | | `excludeFromTabOrder` | `boolean` | Whether to exclude the element from the sequential tab order. If true, the element will not be focusable via the keyboard by tabbing. This should be avoided except in rare scenarios where an alternative means of accessing the element or its functionality via the keyboard is available. | | `preventFocusOnPress` | `boolean` | Whether to prevent focus from moving to the button when pressing it. Caution, this can make the button inaccessible and should only be used when alternative keyboard interaction is provided, such as ComboBox's MenuTrigger or a NumberField's increment/decrement control. | | `aria-expanded` | `boolean | 'true' | 'false'` | Indicates whether the element, or another grouping element it controls, is currently expanded or collapsed. | | `aria-haspopup` | `boolean | 'menu' | 'listbox' | 'tree' | 'grid' | 'dialog' | 'true' | 'false'` | Indicates the availability and type of interactive popup element, such as menu or dialog, that can be triggered by an element. | | `aria-controls` | `string` | Identifies the element (or elements) whose contents or presence are controlled by the current element. | | `aria-pressed` | `boolean | 'true' | 'false' | 'mixed'` | Indicates the current "pressed" state of toggle buttons. | | `aria-current` | `boolean | 'true' | 'false' | 'page' | 'step' | 'location' | 'date' | 'time'` | Indicates whether this element represents the current item within a container or set of related elements. | | `aria-label` | `string` | Defines a string value that labels the current element. | | `aria-labelledby` | `string` | Identifies the element (or elements) that labels the current element. | | `aria-describedby` | `string` | Identifies the element (or elements) that describes the object. | | `aria-details` | `string` | Identifies the element (or elements) that provide a detailed, extended description for the object. | ### DisclosurePanel# Show props | Name | Type | Description | | --- | --- | --- | | `children` | `ReactNode` | The children of the component. | | `className` | `string | ( (values: DisclosurePanelRenderProps & & { defaultClassName: string | | undefined } )) => string` | The CSS className for the element. A function may be provided to compute the class based on component state. | | `style` | `CSSProperties | ( (values: DisclosurePanelRenderProps & & { defaultStyle: CSSProperties } )) => CSSProperties | undefined` | The inline style for the element. A function may be provided to compute the style based on component state. | Accessibility | Name | Type | Default | Description | | --- | --- | --- | --- | | `role` | `'group' | 'region'` | `'group'` | The accessibility role for the disclosure's panel. | | `id` | `string` | — | The element's unique identifier. See MDN. | ## Styling# * * * React Aria components can be styled in many ways, including using CSS classes, inline styles, utility classes (e.g. Tailwind), CSS-in-JS (e.g. Styled Components), etc. By default, all components include a builtin `className` attribute which can be targeted using CSS selectors. These follow the `react-aria-ComponentName` naming convention. .react-aria-DisclosureGroup { /* ... */ } .react-aria-DisclosureGroup { /* ... */ } .react-aria-DisclosureGroup { /* ... */ } A custom `className` can also be specified on any component. This overrides the default `className` provided by React Aria with your own. <DisclosureGroup className="my-accordion"> {/* ... */} </DisclosureGroup> <DisclosureGroup className="my-accordion"> {/* ... */} </DisclosureGroup> <DisclosureGroup className="my-accordion"> {/* ... */} </DisclosureGroup> In addition, some components support multiple UI states (e.g. focused, placeholder, readonly, etc.). React Aria components expose states using data attributes, which you can target in CSS selectors. For example: .react-aria-DisclosureGroup[data-disabled] { /* ... */ } .react-aria-DisclosureGroup[data-disabled] { /* ... */ } .react-aria-DisclosureGroup[data-disabled] { /* ... */ } The `className` and `style` props also accept functions which receive states for styling. This lets you dynamically determine the classes or styles to apply, which is useful when using utility CSS libraries like Tailwind. <Disclosure className={({ isExpanded }) => isExpanded ? 'border-blue-500' : 'border-gray-600'} /> <Disclosure className={({ isExpanded }) => isExpanded ? 'border-blue-500' : 'border-gray-600'} /> <Disclosure className={( { isExpanded } ) => isExpanded ? 'border-blue-500' : 'border-gray-600'} /> The states, selectors, and render props for each component used in a `DisclosureGroup` are documented below. ### DisclosureGroup# A `DisclosureGroup` can be targeted with the `.react-aria-DisclosureGroup` CSS selector, or by overriding with a custom `className`. It supports the following states: | Name | CSS Selector | Description | | --- | --- | --- | | `isDisabled` | `[data-disabled]` | Whether the disclosure group is disabled. | | `state` | `—` | State of the disclosure group. | ### Disclosure# A `Disclosure` can be targeted with the `.react-aria-Disclosure` CSS selector, or by overriding with a custom `className`. It supports the following states: | Name | CSS Selector | Description | | --- | --- | --- | | `isExpanded` | `[data-expanded]` | Whether the disclosure is expanded. | | `isFocusVisibleWithin` | `[data-focus-visible-within]` | Whether the disclosure has keyboard focus. | | `isDisabled` | `[data-disabled]` | Whether the disclosure is disabled. | | `state` | `—` | State of the disclosure. | ### Button# A `Button` can be targeted with the `.react-aria-Button` CSS selector, or by overriding with a custom `className`. It supports the following states: | Name | CSS Selector | Description | | --- | --- | --- | | `isHovered` | `[data-hovered]` | Whether the button is currently hovered with a mouse. | | `isPressed` | `[data-pressed]` | Whether the button is currently in a pressed state. | | `isFocused` | `[data-focused]` | Whether the button is focused, either via a mouse or keyboard. | | `isFocusVisible` | `[data-focus-visible]` | Whether the button is keyboard focused. | | `isDisabled` | `[data-disabled]` | Whether the button is disabled. | | `isPending` | `[data-pending]` | Whether the button is currently in a pending state. | ### DisclosurePanel# A `DisclosurePanel` can be targeted with the `.react-aria-DisclosurePanel` CSS selector, or by overriding with a custom `className`. | Name | CSS Selector | Description | | --- | --- | --- | | `isFocusVisibleWithin` | `[data-focus-visible-within]` | Whether keyboard focus is within the disclosure panel. | ## Advanced Customization# * * * ### State# DisclosureGroup provides a `DisclosureGroupState` object to its children via `DisclosureGroupStateContext`. This can be used to access and manipulate the disclosure group's state. ### Hook# If you need to customize things even further, such as accessing internal state, you can drop down to the lower level Hook-based API. See useDisclosureGroupState for more details. --- ## Page: https://react-spectrum.adobe.com/react-aria/Link.html # Link A link allows a user to navigate to another page or resource within a web page or application. <table><tbody><tr><th>install</th><td><code>yarn add react-aria-components</code></td></tr><tr><th>version</th><td>1.10.1</td></tr><tr><th>usage</th><td><code><span>import</span> {Link} <span>from</span> <span>'react-aria-components'</span></code></td></tr></tbody></table> View ARIA pattern W3C View repository GitHub View package NPM ## Example# * * * import {Link} from 'react-aria-components'; <Link href="https://www.imdb.com/title/tt6348138/" target="_blank"> The missing link </Link> import {Link} from 'react-aria-components'; <Link href="https://www.imdb.com/title/tt6348138/" target="_blank" > The missing link </Link> import {Link} from 'react-aria-components'; <Link href="https://www.imdb.com/title/tt6348138/" target="_blank" > The missing link </Link> The missing link Show CSS @import "@react-aria/example-theme"; .react-aria-Link { color: var(--link-color); font-size: 18px; transition: all 200ms; text-decoration: underline; cursor: pointer; outline: none; position: relative; &[data-hovered] { text-decoration-style: wavy; } &[data-pressed] { color: var(--link-color-pressed); } &[data-focus-visible]:after { content: ''; position: absolute; inset: -3px -6px; border-radius: 6px; border: 2px solid var(--focus-ring-color); } } @import "@react-aria/example-theme"; .react-aria-Link { color: var(--link-color); font-size: 18px; transition: all 200ms; text-decoration: underline; cursor: pointer; outline: none; position: relative; &[data-hovered] { text-decoration-style: wavy; } &[data-pressed] { color: var(--link-color-pressed); } &[data-focus-visible]:after { content: ''; position: absolute; inset: -3px -6px; border-radius: 6px; border: 2px solid var(--focus-ring-color); } } @import "@react-aria/example-theme"; .react-aria-Link { color: var(--link-color); font-size: 18px; transition: all 200ms; text-decoration: underline; cursor: pointer; outline: none; position: relative; &[data-hovered] { text-decoration-style: wavy; } &[data-pressed] { color: var(--link-color-pressed); } &[data-focus-visible]:after { content: ''; position: absolute; inset: -3px -6px; border-radius: 6px; border: 2px solid var(--focus-ring-color); } } ## Features# * * * Links can be created in HTML with the <a> element with an `href` attribute. However, if the link does not have an href, and is handled client side with JavaScript instead, it will not be exposed to assistive technology properly. `Link` helps achieve accessible links with either native HTML elements or custom element types. * **Flexible** – Support for HTML navigation links, JavaScript handled links, and client side routing. Disabled links are also supported. * **Accessible** – Implemented as a custom ARIA link when handled via JavaScript, and otherwise as a native HTML link. * **Styleable** – Hover, press, and keyboard focus states are provided for easy styling. These states only apply when interacting with an appropriate input device, unlike CSS pseudo classes. ## Anatomy# * * * A link consists of a pressable area usually containing a textual label or an icon that users can click or tap to navigate to another page or resource. In addition, keyboard users may activate links using the Enter key. If a visual label is not provided (e.g. an icon or image only link), then an `aria-label` or `aria-labelledby` prop must be passed to identify the link to assistive technology. ## Starter kits# * * * To help kick-start your project, we offer starter kits that include example implementations of all React Aria components with various styling solutions. All components are fully styled, including support for dark mode, high contrast mode, and all UI states. Each starter comes with a pre-configured Storybook that you can experiment with, or use as a starting point for your own component library. Vanilla CSS Download ZIP Preview Tailwind CSS Download ZIP Preview ## Content# * * * Links accept content as children. If the link has an href prop, it will be rendered as an `<a>` element. <Link href="https://adobe.com" target="_blank">Adobe.com</Link> <Link href="https://adobe.com" target="_blank"> Adobe.com </Link> <Link href="https://adobe.com" target="_blank" > Adobe.com </Link> Adobe.com ### JavaScript handled links# When a `<Link`\> does not have an `href` prop, it is rendered as a `<span role="link">` instead of an `<a>`. Events will need to be handled in JavaScript with the `onPress` prop. Note: this will not behave like a native link. Browser features like context menus and open in new tab will not apply. <Link onPress={() => alert('Pressed link')}>Adobe</Link> <Link onPress={() => alert('Pressed link')}>Adobe</Link> <Link onPress={() => alert( 'Pressed link' )} > Adobe </Link> Adobe ## Events# * * * `Link` supports user interactions via mouse, keyboard, and touch. You can handle all of these via the `onPress` prop. This is similar to the standard `onClick` event, but normalized to support all interaction methods equally. In addition, the `onPressStart`, `onPressEnd`, and `onPressChange` events are fired as the user interacts with the link. Each of these handlers receives a `PressEvent` , which exposes information about the target and the type of event that triggered the interaction. See usePress for more details. function Example() { let [pointerType, setPointerType] = React.useState(''); return ( <> <Link onPressStart={(e) => setPointerType(e.pointerType)} onPressEnd={() => setPointerType('')} > Press me </Link> <p> {pointerType ? `You are pressing the link with a ${pointerType}!` : 'Ready to be pressed.'} </p> </> ); } function Example() { let [pointerType, setPointerType] = React.useState(''); return ( <> <Link onPressStart={(e) => setPointerType(e.pointerType)} onPressEnd={() => setPointerType('')} > Press me </Link> <p> {pointerType ? `You are pressing the link with a ${pointerType}!` : 'Ready to be pressed.'} </p> </> ); } function Example() { let [ pointerType, setPointerType ] = React.useState(''); return ( <> <Link onPressStart={(e) => setPointerType( e.pointerType )} onPressEnd={() => setPointerType( '' )} > Press me </Link> <p> {pointerType ? `You are pressing the link with a ${pointerType}!` : 'Ready to be pressed.'} </p> </> ); } Press me Ready to be pressed. ### Client side routing# The `<Link>` component works with frameworks and client side routers like Next.js and React Router. As with other React Aria components that support links, this works via the `RouterProvider` component at the root of your app. See the client side routing guide to learn how to set this up. ## Disabled# * * * A link can be disabled by passing the `isDisabled` property. This will work with both native link elements as well as client handled links. Native navigation will be disabled, and the `onPress` event will not be fired. The link will be exposed as disabled to assistive technology with ARIA. <Link isDisabled href="https://adobe.com" target="_blank">Disabled link</Link> <Link isDisabled href="https://adobe.com" target="_blank"> Disabled link </Link> <Link isDisabled href="https://adobe.com" target="_blank" > Disabled link </Link> Disabled link Show CSS .react-aria-Link { &[data-disabled] { cursor: default; color: var(--text-color-disabled); } } .react-aria-Link { &[data-disabled] { cursor: default; color: var(--text-color-disabled); } } .react-aria-Link { &[data-disabled] { cursor: default; color: var(--text-color-disabled); } } ## Props# * * * | Name | Type | Description | | --- | --- | --- | | `isDisabled` | `boolean` | Whether the link is disabled. | | `autoFocus` | `boolean` | Whether the element should receive focus on render. | | `href` | ` Href ` | A URL to link to. See MDN. | | `hrefLang` | `string` | Hints at the human language of the linked URL. SeeMDN. | | `target` | `HTMLAttributeAnchorTarget` | The target window for the link. See MDN. | | `rel` | `string` | The relationship between the linked resource and the current page. See MDN. | | `download` | `boolean | string` | Causes the browser to download the linked URL. A string may be provided to suggest a file name. See MDN. | | `ping` | `string` | A space-separated list of URLs to ping when the link is followed. See MDN. | | `referrerPolicy` | `HTMLAttributeReferrerPolicy` | How much of the referrer to send when following the link. See MDN. | | `routerOptions` | ` RouterOptions ` | Options for the configured client side router. | | `children` | `ReactNode | ( (values: LinkRenderProps & & { defaultChildren: ReactNode | | undefined } )) => ReactNode` | The children of the component. A function may be provided to alter the children based on component state. | | `className` | `string | ( (values: LinkRenderProps & & { defaultClassName: string | | undefined } )) => string` | The CSS className for the element. A function may be provided to compute the class based on component state. | | `style` | `CSSProperties | ( (values: LinkRenderProps & & { defaultStyle: CSSProperties } )) => CSSProperties | undefined` | The inline style for the element. A function may be provided to compute the style based on component state. | Events | Name | Type | Description | | --- | --- | --- | | `onPress` | `( (e: PressEvent )) => void` | Handler that is called when the press is released over the target. | | `onPressStart` | `( (e: PressEvent )) => void` | Handler that is called when a press interaction starts. | | `onPressEnd` | `( (e: PressEvent )) => void` | Handler that is called when a press interaction ends, either over the target or when the pointer leaves the target. | | `onPressChange` | `( (isPressed: boolean )) => void` | Handler that is called when the press state changes. | | `onPressUp` | `( (e: PressEvent )) => void` | Handler that is called when a press is released over the target, regardless of whether it started on the target or not. | | `onClick` | `( (e: MouseEvent<FocusableElement> )) => void` | **Not recommended – use `onPress` instead.** `onClick` is an alias for `onPress` provided for compatibility with other libraries. `onPress` provides additional event details for non-mouse interactions. | | `onFocus` | `( (e: FocusEvent<Target> )) => void` | Handler that is called when the element receives focus. | | `onBlur` | `( (e: FocusEvent<Target> )) => void` | Handler that is called when the element loses focus. | | `onFocusChange` | `( (isFocused: boolean )) => void` | Handler that is called when the element's focus status changes. | | `onKeyDown` | `( (e: KeyboardEvent )) => void` | Handler that is called when a key is pressed. | | `onKeyUp` | `( (e: KeyboardEvent )) => void` | Handler that is called when a key is released. | | `onHoverStart` | `( (e: HoverEvent )) => void` | Handler that is called when a hover interaction starts. | | `onHoverEnd` | `( (e: HoverEvent )) => void` | Handler that is called when a hover interaction ends. | | `onHoverChange` | `( (isHovering: boolean )) => void` | Handler that is called when the hover state changes. | Layout | Name | Type | Description | | --- | --- | --- | | `slot` | `string | null` | A slot name for the component. Slots allow the component to receive props from a parent component. An explicit `null` value indicates that the local props completely override all props received from a parent. | Accessibility | Name | Type | Description | | --- | --- | --- | | `aria-label` | `string` | Defines a string value that labels the current element. | | `aria-labelledby` | `string` | Identifies the element (or elements) that labels the current element. | | `aria-describedby` | `string` | Identifies the element (or elements) that describes the object. | | `aria-details` | `string` | Identifies the element (or elements) that provide a detailed, extended description for the object. | ## Styling# * * * React Aria components can be styled in many ways, including using CSS classes, inline styles, utility classes (e.g. Tailwind), CSS-in-JS (e.g. Styled Components), etc. By default, all components include a builtin `className` attribute which can be targeted using CSS selectors. These follow the `react-aria-ComponentName` naming convention. .react-aria-Link { /* ... */ } .react-aria-Link { /* ... */ } .react-aria-Link { /* ... */ } A custom `className` can also be specified on any component. This overrides the default `className` provided by React Aria with your own. <Link className="my-link"> {/* ... */} </Link> <Link className="my-link"> {/* ... */} </Link> <Link className="my-link"> {/* ... */} </Link> In addition, some components support multiple UI states (e.g. focused, placeholder, readonly, etc.). React Aria components expose states using data attributes, which you can target in CSS selectors. For example: .react-aria-Link[data-pressed] { /* ... */ } .react-aria-Link[data-pressed] { /* ... */ } .react-aria-Link[data-pressed] { /* ... */ } The `className` and `style` props also accept functions which receive states for styling. This lets you dynamically determine the classes or styles to apply, which is useful when using utility CSS libraries like Tailwind. <Link className={({ isPressed }) => isPressed ? 'bg-gray-700' : 'bg-gray-600'} /> <Link className={({ isPressed }) => isPressed ? 'bg-gray-700' : 'bg-gray-600'} /> <Link className={( { isPressed } ) => isPressed ? 'bg-gray-700' : 'bg-gray-600'} /> Render props may also be used as children to alter what elements are rendered based on the current state. For example, you could render an extra element when the link is in a pressed state. <Link> {({isPressed}) => ( <> {isPressed && <PressHighlight />} Press me </> )} </Link> <Link> {({isPressed}) => ( <> {isPressed && <PressHighlight />} Press me </> )} </Link> <Link> {({ isPressed }) => ( <> {isPressed && ( <PressHighlight /> )} Press me </> )} </Link> The states, selectors, and render props for `Link` are documented below. | Name | CSS Selector | Description | | --- | --- | --- | | `isCurrent` | `[data-current]` | Whether the link is the current item within a list. | | `isHovered` | `[data-hovered]` | Whether the link is currently hovered with a mouse. | | `isPressed` | `[data-pressed]` | Whether the link is currently in a pressed state. | | `isFocused` | `[data-focused]` | Whether the link is focused, either via a mouse or keyboard. | | `isFocusVisible` | `[data-focus-visible]` | Whether the link is keyboard focused. | | `isDisabled` | `[data-disabled]` | Whether the link is disabled. | ## Advanced customization# * * * ### Contexts# All React Aria Components export a corresponding context that can be used to send props to them from a parent element. This enables you to build your own compositional APIs similar to those found in React Aria Components itself. You can send any prop or ref via context that you could pass to the corresponding component. The local props and ref on the component are merged with the ones passed via context, with the local props taking precedence (following the rules documented in mergeProps). | Component | Context | Props | Ref | | --- | --- | --- | --- | | `Link` | `LinkContext` | ` LinkProps ` | `HTMLAnchorElement` | This example shows a `Router` component that accepts `Link` elements as children and keeps track of which one was last clicked. import type {PressEvent} from 'react-aria-components'; import {LinkContext} from 'react-aria-components'; function Router({children}) { let [clicked, setClicked] = React.useState(null); let onPress = (e: PressEvent) => { setClicked(e.target.textContent); }; return ( <LinkContext.Provider value={{onPress}}> {children} {clicked && `You clicked ${clicked}`} </LinkContext.Provider> ); } import type {PressEvent} from 'react-aria-components'; import {LinkContext} from 'react-aria-components'; function Router({children}) { let [clicked, setClicked] = React.useState(null); let onPress = (e: PressEvent) => { setClicked(e.target.textContent); }; return ( <LinkContext.Provider value={{onPress}}> {children} {clicked && `You clicked ${clicked}`} </LinkContext.Provider> ); } import type {PressEvent} from 'react-aria-components'; import {LinkContext} from 'react-aria-components'; function Router( { children } ) { let [ clicked, setClicked ] = React.useState( null ); let onPress = ( e: PressEvent ) => { setClicked( e.target .textContent ); }; return ( <LinkContext.Provider value={{ onPress }} > {children} {clicked && `You clicked ${clicked}`} </LinkContext.Provider> ); } Now any `Link` inside a `Router` will update the router state when it is pressed. <Router> <ul> <li><Link>Breadcrumbs</Link></li> <li><Link>Button</Link></li> <li><Link>Calendar</Link></li> </ul> </Router> <Router> <ul> <li><Link>Breadcrumbs</Link></li> <li><Link>Button</Link></li> <li><Link>Calendar</Link></li> </ul> </Router> <Router> <ul> <li> <Link> Breadcrumbs </Link> </li> <li> <Link> Button </Link> </li> <li> <Link> Calendar </Link> </li> </ul> </Router> * Breadcrumbs * Button * Calendar ### Hooks# If you need to customize things further, such as customizing the DOM structure, you can drop down to the lower level Hook-based API. See useLink for more details. --- ## Page: https://react-spectrum.adobe.com/react-aria/Tabs.html # Tabs Tabs organize content into multiple sections and allow users to navigate between them. <table><tbody><tr><th>install</th><td><code>yarn add react-aria-components</code></td></tr><tr><th>version</th><td>1.10.1</td></tr><tr><th>usage</th><td><code><span>import</span> {Tabs} <span>from</span> <span>'react-aria-components'</span></code></td></tr></tbody></table> View ARIA pattern W3C View repository GitHub View package NPM ## Example# * * * import {Tabs, TabList, Tab, TabPanel} from 'react-aria-components'; <Tabs> <TabList aria-label="History of Ancient Rome"> <Tab id="FoR">Founding of Rome</Tab> <Tab id="MaR">Monarchy and Republic</Tab> <Tab id="Emp">Empire</Tab> </TabList> <TabPanel id="FoR"> Arma virumque cano, Troiae qui primus ab oris. </TabPanel> <TabPanel id="MaR"> Senatus Populusque Romanus. </TabPanel> <TabPanel id="Emp"> Alea jacta est. </TabPanel> </Tabs> import { Tab, TabList, TabPanel, Tabs } from 'react-aria-components'; <Tabs> <TabList aria-label="History of Ancient Rome"> <Tab id="FoR">Founding of Rome</Tab> <Tab id="MaR">Monarchy and Republic</Tab> <Tab id="Emp">Empire</Tab> </TabList> <TabPanel id="FoR"> Arma virumque cano, Troiae qui primus ab oris. </TabPanel> <TabPanel id="MaR"> Senatus Populusque Romanus. </TabPanel> <TabPanel id="Emp"> Alea jacta est. </TabPanel> </Tabs> import { Tab, TabList, TabPanel, Tabs } from 'react-aria-components'; <Tabs> <TabList aria-label="History of Ancient Rome"> <Tab id="FoR"> Founding of Rome </Tab> <Tab id="MaR"> Monarchy and Republic </Tab> <Tab id="Emp"> Empire </Tab> </TabList> <TabPanel id="FoR"> Arma virumque cano, Troiae qui primus ab oris. </TabPanel> <TabPanel id="MaR"> Senatus Populusque Romanus. </TabPanel> <TabPanel id="Emp"> Alea jacta est. </TabPanel> </Tabs> Founding of Rome Monarchy and Republic Empire Arma virumque cano, Troiae qui primus ab oris. Show CSS @import "@react-aria/example-theme"; .react-aria-Tabs { display: flex; color: var(--text-color); &[data-orientation=horizontal] { flex-direction: column; } } .react-aria-TabList { display: flex; &[data-orientation=horizontal] { border-bottom: 1px solid var(--border-color); .react-aria-Tab { border-bottom: 3px solid var(--border-color); } } } .react-aria-Tab { padding: 10px; cursor: default; outline: none; position: relative; color: var(--text-color-base); transition: color 200ms; --border-color: transparent; forced-color-adjust: none; &[data-hovered], &[data-focused] { color: var(--text-color-hover); } &[data-selected] { --border-color: var(--highlight-background); color: var(--text-color); } &[data-disabled] { color: var(--text-color-disabled); &[data-selected] { --border-color: var(--text-color-disabled); } } &[data-focus-visible]:after { content: ''; position: absolute; inset: 4px; border-radius: 4px; border: 2px solid var(--focus-ring-color); } } .react-aria-TabPanel { margin-top: 4px; padding: 10px; border-radius: 4px; outline: none; &[data-focus-visible] { outline: 2px solid var(--focus-ring-color); } } @import "@react-aria/example-theme"; .react-aria-Tabs { display: flex; color: var(--text-color); &[data-orientation=horizontal] { flex-direction: column; } } .react-aria-TabList { display: flex; &[data-orientation=horizontal] { border-bottom: 1px solid var(--border-color); .react-aria-Tab { border-bottom: 3px solid var(--border-color); } } } .react-aria-Tab { padding: 10px; cursor: default; outline: none; position: relative; color: var(--text-color-base); transition: color 200ms; --border-color: transparent; forced-color-adjust: none; &[data-hovered], &[data-focused] { color: var(--text-color-hover); } &[data-selected] { --border-color: var(--highlight-background); color: var(--text-color); } &[data-disabled] { color: var(--text-color-disabled); &[data-selected] { --border-color: var(--text-color-disabled); } } &[data-focus-visible]:after { content: ''; position: absolute; inset: 4px; border-radius: 4px; border: 2px solid var(--focus-ring-color); } } .react-aria-TabPanel { margin-top: 4px; padding: 10px; border-radius: 4px; outline: none; &[data-focus-visible] { outline: 2px solid var(--focus-ring-color); } } @import "@react-aria/example-theme"; .react-aria-Tabs { display: flex; color: var(--text-color); &[data-orientation=horizontal] { flex-direction: column; } } .react-aria-TabList { display: flex; &[data-orientation=horizontal] { border-bottom: 1px solid var(--border-color); .react-aria-Tab { border-bottom: 3px solid var(--border-color); } } } .react-aria-Tab { padding: 10px; cursor: default; outline: none; position: relative; color: var(--text-color-base); transition: color 200ms; --border-color: transparent; forced-color-adjust: none; &[data-hovered], &[data-focused] { color: var(--text-color-hover); } &[data-selected] { --border-color: var(--highlight-background); color: var(--text-color); } &[data-disabled] { color: var(--text-color-disabled); &[data-selected] { --border-color: var(--text-color-disabled); } } &[data-focus-visible]:after { content: ''; position: absolute; inset: 4px; border-radius: 4px; border: 2px solid var(--focus-ring-color); } } .react-aria-TabPanel { margin-top: 4px; padding: 10px; border-radius: 4px; outline: none; &[data-focus-visible] { outline: 2px solid var(--focus-ring-color); } } ## Features# * * * Tabs provide a list of tabs that a user can select from to switch between multiple tab panels. `Tabs` can be used to implement these in an accessible way. * **Flexible** – Support for both horizontal and vertical orientations, disabled tabs, customizable layout, and multiple keyboard activation modes. * **Accessible** – Follows the ARIA tabs pattern, automatically linking tabs and their associated tab panels semantically. The arrow keys can be used to navigate between tabs, and tab panels automatically become focusable when they don't contain any focusable children. * **International** – Keyboard navigation is automatically mirrored in right-to-left languages. * **Styleable** – Hover, press, keyboard focus, and selection states are provided for easy styling. These states only apply when interacting with an appropriate input device, unlike CSS pseudo classes. ## Anatomy# * * * Tabs consist of a tab list with one or more visually separated tabs. Each tab has associated content, and only the selected tab's content is shown. Each tab can be clicked, tapped, or navigated to via arrow keys. Depending on the `keyboardActivation` prop, the tab can be selected by receiving keyboard focus, or it can be selected with the Enter key. import {Tabs, TabList, Tab, TabPanel} from 'react-aria-components'; <Tabs> <TabList> <Tab /> </TabList> <TabPanel /> </Tabs> import { Tab, TabList, TabPanel, Tabs } from 'react-aria-components'; <Tabs> <TabList> <Tab /> </TabList> <TabPanel /> </Tabs> import { Tab, TabList, TabPanel, Tabs } from 'react-aria-components'; <Tabs> <TabList> <Tab /> </TabList> <TabPanel /> </Tabs> ### Concepts# `Tabs` makes use of the following concepts: Collections Defining collections of items, async loading, and updating items over time. Selection Interactions and data structures to represent selection. ## Examples# * * * Category Tabs An article category tabs component styled with Tailwind CSS. Swipeable Tabs A swipeable Tabs component built with Framer Motion and CSS scroll snapping. ## Starter kits# * * * To help kick-start your project, we offer starter kits that include example implementations of all React Aria components with various styling solutions. All components are fully styled, including support for dark mode, high contrast mode, and all UI states. Each starter comes with a pre-configured Storybook that you can experiment with, or use as a starting point for your own component library. Vanilla CSS Download ZIP Preview Tailwind CSS Download ZIP Preview ## Selection# * * * ### Default selection# A default selected tab can be provided using the `defaultSelectedKey` prop, which should correspond to the `id` prop provided to each item. When `Tabs` is used with dynamic items as described below, the key of each item is derived from the data. See the Selection guide for more details. <Tabs defaultSelectedKey="keyboard"> <TabList aria-label="Input settings"> <Tab id="mouse">Mouse Settings</Tab> <Tab id="keyboard">Keyboard Settings</Tab> <Tab id="gamepad">Gamepad Settings</Tab> </TabList> <TabPanel id="mouse">Mouse Settings</TabPanel> <TabPanel id="keyboard">Keyboard Settings</TabPanel> <TabPanel id="gamepad">Gamepad Settings</TabPanel> </Tabs> <Tabs defaultSelectedKey="keyboard"> <TabList aria-label="Input settings"> <Tab id="mouse">Mouse Settings</Tab> <Tab id="keyboard">Keyboard Settings</Tab> <Tab id="gamepad">Gamepad Settings</Tab> </TabList> <TabPanel id="mouse">Mouse Settings</TabPanel> <TabPanel id="keyboard">Keyboard Settings</TabPanel> <TabPanel id="gamepad">Gamepad Settings</TabPanel> </Tabs> <Tabs defaultSelectedKey="keyboard"> <TabList aria-label="Input settings"> <Tab id="mouse"> Mouse Settings </Tab> <Tab id="keyboard"> Keyboard Settings </Tab> <Tab id="gamepad"> Gamepad Settings </Tab> </TabList> <TabPanel id="mouse"> Mouse Settings </TabPanel> <TabPanel id="keyboard"> Keyboard Settings </TabPanel> <TabPanel id="gamepad"> Gamepad Settings </TabPanel> </Tabs> Mouse Settings Keyboard Settings Gamepad Settings Keyboard Settings ### Controlled selection# Selection can be controlled using the `selectedKey` prop, paired with the `onSelectionChange` event. The `id` prop from the selected tab will be passed into the callback when the tab is selected, allowing you to update state accordingly. import type {Key} from 'react-aria-components'; function Example() { let [timePeriod, setTimePeriod] = React.useState<Key>('triassic'); return ( <> <p>Selected time period: {timePeriod}</p> <Tabs selectedKey={timePeriod} onSelectionChange={setTimePeriod}> <TabList aria-label="Mesozoic time periods"> <Tab id="triassic">Triassic</Tab> <Tab id="jurassic">Jurassic</Tab> <Tab id="cretaceous">Cretaceous</Tab> </TabList> <TabPanel id="triassic"> The Triassic ranges roughly from 252 million to 201 million years ago, preceding the Jurassic Period. </TabPanel> <TabPanel id="jurassic"> The Jurassic ranges from 200 million years to 145 million years ago. </TabPanel> <TabPanel id="cretaceous"> The Cretaceous is the longest period of the Mesozoic, spanning from 145 million to 66 million years ago. </TabPanel> </Tabs> </> ); } import type {Key} from 'react-aria-components'; function Example() { let [timePeriod, setTimePeriod] = React.useState<Key>( 'triassic' ); return ( <> <p>Selected time period: {timePeriod}</p> <Tabs selectedKey={timePeriod} onSelectionChange={setTimePeriod} > <TabList aria-label="Mesozoic time periods"> <Tab id="triassic">Triassic</Tab> <Tab id="jurassic">Jurassic</Tab> <Tab id="cretaceous">Cretaceous</Tab> </TabList> <TabPanel id="triassic"> The Triassic ranges roughly from 252 million to 201 million years ago, preceding the Jurassic Period. </TabPanel> <TabPanel id="jurassic"> The Jurassic ranges from 200 million years to 145 million years ago. </TabPanel> <TabPanel id="cretaceous"> The Cretaceous is the longest period of the Mesozoic, spanning from 145 million to 66 million years ago. </TabPanel> </Tabs> </> ); } import type {Key} from 'react-aria-components'; function Example() { let [ timePeriod, setTimePeriod ] = React.useState< Key >('triassic'); return ( <> <p> Selected time period:{' '} {timePeriod} </p> <Tabs selectedKey={timePeriod} onSelectionChange={setTimePeriod} > <TabList aria-label="Mesozoic time periods"> <Tab id="triassic"> Triassic </Tab> <Tab id="jurassic"> Jurassic </Tab> <Tab id="cretaceous"> Cretaceous </Tab> </TabList> <TabPanel id="triassic"> The Triassic ranges roughly from 252 million to 201 million years ago, preceding the Jurassic Period. </TabPanel> <TabPanel id="jurassic"> The Jurassic ranges from 200 million years to 145 million years ago. </TabPanel> <TabPanel id="cretaceous"> The Cretaceous is the longest period of the Mesozoic, spanning from 145 million to 66 million years ago. </TabPanel> </Tabs> </> ); } Selected time period: triassic Triassic Jurassic Cretaceous The Triassic ranges roughly from 252 million to 201 million years ago, preceding the Jurassic Period. ### Keyboard Activation# By default, pressing the arrow keys while focus is on a Tab will switch selection to the adjacent Tab in that direction, updating the content displayed accordingly. If you would like to prevent selection change from happening automatically you can set the `keyboardActivation` prop to "manual". This will prevent tab selection from changing on arrow key press, requiring a subsequent `Enter` or `Space` key press to confirm tab selection. <Tabs keyboardActivation="manual"> <TabList aria-label="Input settings"> <Tab id="mouse">Mouse Settings</Tab> <Tab id="keyboard">Keyboard Settings</Tab> <Tab id="gamepad">Gamepad Settings</Tab> </TabList> <TabPanel id="mouse">Mouse Settings</TabPanel> <TabPanel id="keyboard">Keyboard Settings</TabPanel> <TabPanel id="gamepad">Gamepad Settings</TabPanel> </Tabs> <Tabs keyboardActivation="manual"> <TabList aria-label="Input settings"> <Tab id="mouse">Mouse Settings</Tab> <Tab id="keyboard">Keyboard Settings</Tab> <Tab id="gamepad">Gamepad Settings</Tab> </TabList> <TabPanel id="mouse">Mouse Settings</TabPanel> <TabPanel id="keyboard">Keyboard Settings</TabPanel> <TabPanel id="gamepad">Gamepad Settings</TabPanel> </Tabs> <Tabs keyboardActivation="manual"> <TabList aria-label="Input settings"> <Tab id="mouse"> Mouse Settings </Tab> <Tab id="keyboard"> Keyboard Settings </Tab> <Tab id="gamepad"> Gamepad Settings </Tab> </TabList> <TabPanel id="mouse"> Mouse Settings </TabPanel> <TabPanel id="keyboard"> Keyboard Settings </TabPanel> <TabPanel id="gamepad"> Gamepad Settings </TabPanel> </Tabs> Mouse Settings Keyboard Settings Gamepad Settings Mouse Settings ## Content# * * * ### Focusable content# When the tab panel doesn't contain any focusable content, the entire panel is given a `tabIndex=0` so that the content can be navigated to with the keyboard. When the tab panel contains focusable content, such as a textfield, then the `tabIndex` is omitted because the content itself can receive focus. This example uses the same `Tabs` component from above. Try navigating from the tabs to the content for each panel using the keyboard. <Tabs> <TabList aria-label="Notes app"> <Tab id="1">Jane Doe</Tab> <Tab id="2">John Doe</Tab> <Tab id="3">Joe Bloggs</Tab> </TabList> <TabPanel id="1"> <label>Leave a note for Jane: <input type="text" /></label> </TabPanel> <TabPanel id="2">Senatus Populusque Romanus.</TabPanel> <TabPanel id="3">Alea jacta est.</TabPanel> </Tabs> <Tabs> <TabList aria-label="Notes app"> <Tab id="1">Jane Doe</Tab> <Tab id="2">John Doe</Tab> <Tab id="3">Joe Bloggs</Tab> </TabList> <TabPanel id="1"> <label> Leave a note for Jane: <input type="text" /> </label> </TabPanel> <TabPanel id="2">Senatus Populusque Romanus.</TabPanel> <TabPanel id="3">Alea jacta est.</TabPanel> </Tabs> <Tabs> <TabList aria-label="Notes app"> <Tab id="1"> Jane Doe </Tab> <Tab id="2"> John Doe </Tab> <Tab id="3"> Joe Bloggs </Tab> </TabList> <TabPanel id="1"> <label> Leave a note for Jane:{' '} <input type="text" /> </label> </TabPanel> <TabPanel id="2"> Senatus Populusque Romanus. </TabPanel> <TabPanel id="3"> Alea jacta est. </TabPanel> </Tabs> Jane Doe John Doe Joe Bloggs Leave a note for Jane: ### Dynamic items# The above examples have shown tabs with static items. The `items` prop can be used when creating tabs from a dynamic collection, for example when the user can add and remove tabs, or the tabs come from an external data source. The function passed as the children of the `TabList` component is called for each item in the list, and returns an `<Tab>`. A function passed as the children of the `Collection` component returns a corresponding `<TabPanel>` for each tab. Each item accepts an `id` prop, which is passed to the `onSelectionChange` handler to identify the selected item. Alternatively, if the item objects contain an `id` property, as shown in the example below, then this is used automatically and an `id` prop is not required. See Collection Components for more details. import {Collection, Button} from 'react-aria-components'; function Example() { let [tabs, setTabs] = React.useState([ {id: 1, title: 'Tab 1', content: 'Tab body 1'}, {id: 2, title: 'Tab 2', content: 'Tab body 2'}, {id: 3, title: 'Tab 3', content: 'Tab body 3'} ]); let addTab = () => { setTabs(tabs => [ ...tabs, { id: tabs.length + 1, title: `Tab ${tabs.length + 1}`, content: `Tab body ${tabs.length + 1}` } ]); }; let removeTab = () => { if (tabs.length > 1) { setTabs(tabs => tabs.slice(0, -1)); } }; return ( <Tabs> <div style={{display: 'flex'}}> <TabList aria-label="Dynamic tabs" items={tabs} style={{flex: 1}}> {item => <Tab>{item.title}</Tab>} </TabList> <div className="button-group"> <Button onPress={addTab}>Add tab</Button> <Button onPress={removeTab}>Remove tab</Button> </div> </div> <Collection items={tabs}> {item => <TabPanel>{item.content}</TabPanel>} </Collection> </Tabs> ); } import {Button, Collection} from 'react-aria-components'; function Example() { let [tabs, setTabs] = React.useState([ { id: 1, title: 'Tab 1', content: 'Tab body 1' }, { id: 2, title: 'Tab 2', content: 'Tab body 2' }, { id: 3, title: 'Tab 3', content: 'Tab body 3' } ]); let addTab = () => { setTabs((tabs) => [ ...tabs, { id: tabs.length + 1, title: `Tab ${tabs.length + 1}`, content: `Tab body ${tabs.length + 1}` } ]); }; let removeTab = () => { if (tabs.length > 1) { setTabs((tabs) => tabs.slice(0, -1)); } }; return ( <Tabs> <div style={{ display: 'flex' }}> <TabList aria-label="Dynamic tabs" items={tabs} style={{ flex: 1 }} > {(item) => <Tab>{item.title}</Tab>} </TabList> <div className="button-group"> <Button onPress={addTab}>Add tab</Button> <Button onPress={removeTab}>Remove tab</Button> </div> </div> <Collection items={tabs}> {(item) => <TabPanel>{item.content}</TabPanel>} </Collection> </Tabs> ); } import { Button, Collection } from 'react-aria-components'; function Example() { let [tabs, setTabs] = React.useState([ { id: 1, title: 'Tab 1', content: 'Tab body 1' }, { id: 2, title: 'Tab 2', content: 'Tab body 2' }, { id: 3, title: 'Tab 3', content: 'Tab body 3' } ]); let addTab = () => { setTabs((tabs) => [ ...tabs, { id: tabs.length + 1, title: `Tab ${ tabs.length + 1 }`, content: `Tab body ${ tabs.length + 1 }` } ]); }; let removeTab = () => { if ( tabs.length > 1 ) { setTabs((tabs) => tabs.slice(0, -1) ); } }; return ( <Tabs> <div style={{ display: 'flex' }} > <TabList aria-label="Dynamic tabs" items={tabs} style={{ flex: 1 }} > {(item) => ( <Tab> {item .title} </Tab> )} </TabList> <div className="button-group"> <Button onPress={addTab} > Add tab </Button> <Button onPress={removeTab} > Remove tab </Button> </div> </div> <Collection items={tabs} > {(item) => ( <TabPanel> {item .content} </TabPanel> )} </Collection> </Tabs> ); } Tab 1 Tab 2 Tab 3 Add tabRemove tab Tab body 1 Show CSS .button-group { border-bottom: 1px solid gray; display: flex; align-items: center; gap: 8px; } .button-group { border-bottom: 1px solid gray; display: flex; align-items: center; gap: 8px; } .button-group { border-bottom: 1px solid gray; display: flex; align-items: center; gap: 8px; } ## Orientation# * * * By default, tabs are horizontally oriented. The `orientation` prop can be set to `vertical` to change this. This does not affect keyboard navigation. You are responsible for styling your tabs accordingly. <Tabs orientation="vertical"> <TabList aria-label="Chat log orientation example"> <Tab id="1">John Doe</Tab> <Tab id="2">Jane Doe</Tab> <Tab id="3">Joe Bloggs</Tab> </TabList> <TabPanel id="1">There is no prior chat history with John Doe.</TabPanel> <TabPanel id="2">There is no prior chat history with Jane Doe.</TabPanel> <TabPanel id="3">There is no prior chat history with Joe Bloggs.</TabPanel> </Tabs> <Tabs orientation="vertical"> <TabList aria-label="Chat log orientation example"> <Tab id="1">John Doe</Tab> <Tab id="2">Jane Doe</Tab> <Tab id="3">Joe Bloggs</Tab> </TabList> <TabPanel id="1"> There is no prior chat history with John Doe. </TabPanel> <TabPanel id="2"> There is no prior chat history with Jane Doe. </TabPanel> <TabPanel id="3"> There is no prior chat history with Joe Bloggs. </TabPanel> </Tabs> <Tabs orientation="vertical"> <TabList aria-label="Chat log orientation example"> <Tab id="1"> John Doe </Tab> <Tab id="2"> Jane Doe </Tab> <Tab id="3"> Joe Bloggs </Tab> </TabList> <TabPanel id="1"> There is no prior chat history with John Doe. </TabPanel> <TabPanel id="2"> There is no prior chat history with Jane Doe. </TabPanel> <TabPanel id="3"> There is no prior chat history with Joe Bloggs. </TabPanel> </Tabs> John Doe Jane Doe Joe Bloggs There is no prior chat history with John Doe. Show CSS .react-aria-Tabs { &[data-orientation=vertical] { flex-direction: row; } } .react-aria-TabList { &[data-orientation=vertical] { flex-direction: column; border-inline-end: 1px solid gray; .react-aria-Tab { border-inline-end: 3px solid var(--border-color, transparent); } } } .react-aria-Tabs { &[data-orientation=vertical] { flex-direction: row; } } .react-aria-TabList { &[data-orientation=vertical] { flex-direction: column; border-inline-end: 1px solid gray; .react-aria-Tab { border-inline-end: 3px solid var(--border-color, transparent); } } } .react-aria-Tabs { &[data-orientation=vertical] { flex-direction: row; } } .react-aria-TabList { &[data-orientation=vertical] { flex-direction: column; border-inline-end: 1px solid gray; .react-aria-Tab { border-inline-end: 3px solid var(--border-color, transparent); } } } ## Disabled# * * * All tabs can be disabled using the `isDisabled` prop. <Tabs isDisabled> <TabList aria-label="Input settings"> <Tab id="mouse">Mouse Settings</Tab> <Tab id="keyboard">Keyboard Settings</Tab> <Tab id="gamepad">Gamepad Settings</Tab> </TabList> <TabPanel id="mouse">Mouse Settings</TabPanel> <TabPanel id="keyboard">Keyboard Settings</TabPanel> <TabPanel id="gamepad">Gamepad Settings</TabPanel> </Tabs> <Tabs isDisabled> <TabList aria-label="Input settings"> <Tab id="mouse">Mouse Settings</Tab> <Tab id="keyboard">Keyboard Settings</Tab> <Tab id="gamepad">Gamepad Settings</Tab> </TabList> <TabPanel id="mouse">Mouse Settings</TabPanel> <TabPanel id="keyboard">Keyboard Settings</TabPanel> <TabPanel id="gamepad">Gamepad Settings</TabPanel> </Tabs> <Tabs isDisabled> <TabList aria-label="Input settings"> <Tab id="mouse"> Mouse Settings </Tab> <Tab id="keyboard"> Keyboard Settings </Tab> <Tab id="gamepad"> Gamepad Settings </Tab> </TabList> <TabPanel id="mouse"> Mouse Settings </TabPanel> <TabPanel id="keyboard"> Keyboard Settings </TabPanel> <TabPanel id="gamepad"> Gamepad Settings </TabPanel> </Tabs> Mouse Settings Keyboard Settings Gamepad Settings Mouse Settings Show CSS .react-aria-Tab { &[data-disabled] { color: var(--text-color-disabled); &[data-selected] { --border-color: var(--border-color-disabled); } } } .react-aria-Tab { &[data-disabled] { color: var(--text-color-disabled); &[data-selected] { --border-color: var(--border-color-disabled); } } } .react-aria-Tab { &[data-disabled] { color: var(--text-color-disabled); &[data-selected] { --border-color: var(--border-color-disabled); } } } ### Disabled items# An individual `Tab` can be disabled with the `isDisabled` prop. Disabled tabs are not focusable, selectable, or keyboard navigable. <Tabs> <TabList aria-label="Input settings"> <Tab id="mouse">Mouse Settings</Tab> <Tab id="keyboard">Keyboard Settings</Tab> <Tab id="gamepad" isDisabled>Gamepad Settings</Tab> </TabList> <TabPanel id="mouse">Mouse Settings</TabPanel> <TabPanel id="keyboard">Keyboard Settings</TabPanel> <TabPanel id="gamepad">Gamepad Settings</TabPanel> </Tabs> <Tabs> <TabList aria-label="Input settings"> <Tab id="mouse">Mouse Settings</Tab> <Tab id="keyboard">Keyboard Settings</Tab> <Tab id="gamepad" isDisabled>Gamepad Settings</Tab> </TabList> <TabPanel id="mouse">Mouse Settings</TabPanel> <TabPanel id="keyboard">Keyboard Settings</TabPanel> <TabPanel id="gamepad">Gamepad Settings</TabPanel> </Tabs> <Tabs> <TabList aria-label="Input settings"> <Tab id="mouse"> Mouse Settings </Tab> <Tab id="keyboard"> Keyboard Settings </Tab> <Tab id="gamepad" isDisabled > Gamepad Settings </Tab> </TabList> <TabPanel id="mouse"> Mouse Settings </TabPanel> <TabPanel id="keyboard"> Keyboard Settings </TabPanel> <TabPanel id="gamepad"> Gamepad Settings </TabPanel> </Tabs> Mouse Settings Keyboard Settings Gamepad Settings Mouse Settings In dynamic collections, it may be more convenient to use the `disabledKeys` prop at the `Tabs` level instead of `isDisabled` on individual tabs. Each key in this list corresponds with the `id` prop passed to the `Tab` component, or automatically derived from the values passed to the `items` prop (see the Collections for more details). A tab is considered disabled if its id exists in `disabledKeys` or if it has `isDisabled`. function Example() { let tabs = [ {id: 1, title: 'Mouse settings'}, {id: 2, title: 'Keyboard settings'}, {id: 3, title: 'Gamepad settings'} ]; return ( <Tabs disabledKeys={[2]}> <TabList aria-label="Input settings" items={tabs}> {item => <Tab>{item.title}</Tab>} </TabList> <Collection items={tabs}> {item => <TabPanel>{item.title}</TabPanel>} </Collection> </Tabs> ); } function Example() { let tabs = [ {id: 1, title: 'Mouse settings'}, {id: 2, title: 'Keyboard settings'}, {id: 3, title: 'Gamepad settings'} ]; return ( <Tabs disabledKeys={[2]}> <TabList aria-label="Input settings" items={tabs}> {item => <Tab>{item.title}</Tab>} </TabList> <Collection items={tabs}> {item => <TabPanel>{item.title}</TabPanel>} </Collection> </Tabs> ); } function Example() { let tabs = [ { id: 1, title: 'Mouse settings' }, { id: 2, title: 'Keyboard settings' }, { id: 3, title: 'Gamepad settings' } ]; return ( <Tabs disabledKeys={[2]} > <TabList aria-label="Input settings" items={tabs} > {(item) => ( <Tab> {item.title} </Tab> )} </TabList> <Collection items={tabs} > {(item) => ( <TabPanel> {item.title} </TabPanel> )} </Collection> </Tabs> ); } Mouse settings Keyboard settings Gamepad settings Mouse settings ## Links# * * * Tabs may be rendered as links to different routes in your application. This can be achieved by passing the `href` prop to the `<Tab>` component. By default, links perform native browser navigation. However, you'll usually want to synchronize the selected tab with the URL from your client side router. This takes two steps: 1. Set up a` RouterProvider `at the root of your app. This will handle link navigation from all React Aria components using your framework or router. See the client side routing guide to learn how to set this up. 2. Use the `selectedKey` prop to set the selected tab based on the URL, as described above. This example uses React Router to setup routes for each tab and synchronize the selection with the URL. import {BrowserRouter, Route, Routes, useLocation, useNavigate} from 'react-router-dom'; import {RouterProvider} from 'react-aria-components'; function AppTabs() { let { pathname } = useLocation(); return ( <Tabs selectedKey={pathname}> <TabList aria-label="Tabs"> <Tab id="/" href="/">Home</Tab> <Tab id="/shared" href="/shared">Shared</Tab> <Tab id="/deleted" href="/deleted">Deleted</Tab> </TabList> <TabPanel id={pathname}> <Routes> <Route path="/" element={<HomePage />} /> <Route path="/shared" element={<SharedPage />} /> <Route path="/deleted" element={<DeletedPage />} /> </Routes> </TabPanel> </Tabs> ); } function App() { let navigate = useNavigate(); return ( <RouterProvider navigate={navigate}> <Routes> <Route path="/*" element={<AppTabs />} /> </Routes> </RouterProvider> ); } <BrowserRouter> <App /> </BrowserRouter> import { BrowserRouter, Route, Routes, useLocation, useNavigate } from 'react-router-dom'; import {RouterProvider} from 'react-aria-components'; function AppTabs() { let { pathname } = useLocation(); return ( <Tabs selectedKey={pathname}> <TabList aria-label="Tabs"> <Tab id="/" href="/">Home</Tab> <Tab id="/shared" href="/shared">Shared</Tab> <Tab id="/deleted" href="/deleted">Deleted</Tab> </TabList> <TabPanel id={pathname}> <Routes> <Route path="/" element={<HomePage />} /> <Route path="/shared" element={<SharedPage />} /> <Route path="/deleted" element={<DeletedPage />} /> </Routes> </TabPanel> </Tabs> ); } function App() { let navigate = useNavigate(); return ( <RouterProvider navigate={navigate}> <Routes> <Route path="/*" element={<AppTabs />} /> </Routes> </RouterProvider> ); } <BrowserRouter> <App /> </BrowserRouter> import { BrowserRouter, Route, Routes, useLocation, useNavigate } from 'react-router-dom'; import {RouterProvider} from 'react-aria-components'; function AppTabs() { let { pathname } = useLocation(); return ( <Tabs selectedKey={pathname} > <TabList aria-label="Tabs"> <Tab id="/" href="/" > Home </Tab> <Tab id="/shared" href="/shared" > Shared </Tab> <Tab id="/deleted" href="/deleted" > Deleted </Tab> </TabList> <TabPanel id={pathname} > <Routes> <Route path="/" element={ <HomePage /> } /> <Route path="/shared" element={ <SharedPage /> } /> <Route path="/deleted" element={ <DeletedPage /> } /> </Routes> </TabPanel> </Tabs> ); } function App() { let navigate = useNavigate(); return ( <RouterProvider navigate={navigate} > <Routes> <Route path="/*" element={ <AppTabs /> } /> </Routes> </RouterProvider> ); } <BrowserRouter> <App /> </BrowserRouter> ## Props# * * * ### Tabs# | Name | Type | Default | Description | | --- | --- | --- | --- | | `isDisabled` | `boolean` | — | Whether the TabList is disabled. Shows that a selection exists, but is not available in that circumstance. | | `disabledKeys` | `Iterable<Key>` | — | The item keys that are disabled. These items cannot be selected, focused, or otherwise interacted with. | | `selectedKey` | `Key | null` | — | The currently selected key in the collection (controlled). | | `defaultSelectedKey` | `Key` | — | The initial selected key in the collection (uncontrolled). | | `keyboardActivation` | `'automatic' | 'manual'` | `'automatic'` | Whether tabs are activated automatically on focus or manually. | | `orientation` | ` Orientation ` | `'horizontal'` | The orientation of the tabs. | | `children` | `ReactNode | ( (values: TabsRenderProps & & { defaultChildren: ReactNode | | undefined } )) => ReactNode` | — | The children of the component. A function may be provided to alter the children based on component state. | | `className` | `string | ( (values: TabsRenderProps & & { defaultClassName: string | | undefined } )) => string` | — | The CSS className for the element. A function may be provided to compute the class based on component state. | | `style` | `CSSProperties | ( (values: TabsRenderProps & & { defaultStyle: CSSProperties } )) => CSSProperties | undefined` | — | The inline style for the element. A function may be provided to compute the style based on component state. | Events | Name | Type | Description | | --- | --- | --- | | `onSelectionChange` | `( (key: Key )) => void` | Handler that is called when the selection changes. | Layout | Name | Type | Description | | --- | --- | --- | | `slot` | `string | null` | A slot name for the component. Slots allow the component to receive props from a parent component. An explicit `null` value indicates that the local props completely override all props received from a parent. | Accessibility | Name | Type | Description | | --- | --- | --- | | `id` | `string` | The element's unique identifier. See MDN. | | `aria-label` | `string` | Defines a string value that labels the current element. | | `aria-labelledby` | `string` | Identifies the element (or elements) that labels the current element. | | `aria-describedby` | `string` | Identifies the element (or elements) that describes the object. | | `aria-details` | `string` | Identifies the element (or elements) that provide a detailed, extended description for the object. | ### TabList# | Name | Type | Description | | --- | --- | --- | | `className` | `string | ( (values: TabListRenderProps & & { defaultClassName: string | | undefined } )) => string` | The CSS className for the element. A function may be provided to compute the class based on component state. | | `style` | `CSSProperties | ( (values: TabListRenderProps & & { defaultStyle: CSSProperties } )) => CSSProperties | undefined` | The inline style for the element. A function may be provided to compute the style based on component state. | | `children` | `ReactNode | ( (item: T )) => ReactNode` | The contents of the collection. | | `dependencies` | `ReadonlyArray<any>` | Values that should invalidate the item cache when using dynamic collections. | | `items` | `Iterable<T>` | Item objects in the collection. | Accessibility | Name | Type | Description | | --- | --- | --- | | `aria-label` | `string` | Defines a string value that labels the current element. | | `aria-labelledby` | `string` | Identifies the element (or elements) that labels the current element. | | `aria-describedby` | `string` | Identifies the element (or elements) that describes the object. | | `aria-details` | `string` | Identifies the element (or elements) that provide a detailed, extended description for the object. | ### Tab# | Name | Type | Description | | --- | --- | --- | | `id` | `Key` | The unique id of the tab. | | `isDisabled` | `boolean` | Whether the tab is disabled. | | `children` | `ReactNode | ( (values: TabRenderProps & & { defaultChildren: ReactNode | | undefined } )) => ReactNode` | The children of the component. A function may be provided to alter the children based on component state. | | `className` | `string | ( (values: TabRenderProps & & { defaultClassName: string | | undefined } )) => string` | The CSS className for the element. A function may be provided to compute the class based on component state. | | `style` | `CSSProperties | ( (values: TabRenderProps & & { defaultStyle: CSSProperties } )) => CSSProperties | undefined` | The inline style for the element. A function may be provided to compute the style based on component state. | | `href` | ` Href ` | A URL to link to. See MDN. | | `hrefLang` | `string` | Hints at the human language of the linked URL. SeeMDN. | | `target` | `HTMLAttributeAnchorTarget` | The target window for the link. See MDN. | | `rel` | `string` | The relationship between the linked resource and the current page. See MDN. | | `download` | `boolean | string` | Causes the browser to download the linked URL. A string may be provided to suggest a file name. See MDN. | | `ping` | `string` | A space-separated list of URLs to ping when the link is followed. See MDN. | | `referrerPolicy` | `HTMLAttributeReferrerPolicy` | How much of the referrer to send when following the link. See MDN. | | `routerOptions` | ` RouterOptions ` | Options for the configured client side router. | Events | Name | Type | Description | | --- | --- | --- | | `onHoverStart` | `( (e: HoverEvent )) => void` | Handler that is called when a hover interaction starts. | | `onHoverEnd` | `( (e: HoverEvent )) => void` | Handler that is called when a hover interaction ends. | | `onHoverChange` | `( (isHovering: boolean )) => void` | Handler that is called when the hover state changes. | Accessibility | Name | Type | Description | | --- | --- | --- | | `aria-label` | `string` | Defines a string value that labels the current element. | | `aria-labelledby` | `string` | Identifies the element (or elements) that labels the current element. | | `aria-describedby` | `string` | Identifies the element (or elements) that describes the object. | | `aria-details` | `string` | Identifies the element (or elements) that provide a detailed, extended description for the object. | ### TabPanel# | Name | Type | Default | Description | | --- | --- | --- | --- | | `shouldForceMount` | `boolean` | `false` | Whether to mount the tab panel in the DOM even when it is not currently selected. Inactive tab panels are inert and cannot be interacted with. They must be styled appropriately so this is clear to the user visually. | | `children` | `ReactNode | ( (values: TabPanelRenderProps & & { defaultChildren: ReactNode | | undefined } )) => ReactNode` | — | The children of the component. A function may be provided to alter the children based on component state. | | `className` | `string | ( (values: TabPanelRenderProps & & { defaultClassName: string | | undefined } )) => string` | — | The CSS className for the element. A function may be provided to compute the class based on component state. | | `style` | `CSSProperties | ( (values: TabPanelRenderProps & & { defaultStyle: CSSProperties } )) => CSSProperties | undefined` | — | The inline style for the element. A function may be provided to compute the style based on component state. | Accessibility | Name | Type | Description | | --- | --- | --- | | `id` | `string` | The element's unique identifier. See MDN. | | `aria-label` | `string` | Defines a string value that labels the current element. | | `aria-labelledby` | `string` | Identifies the element (or elements) that labels the current element. | | `aria-describedby` | `string` | Identifies the element (or elements) that describes the object. | | `aria-details` | `string` | Identifies the element (or elements) that provide a detailed, extended description for the object. | ## Styling# * * * React Aria components can be styled in many ways, including using CSS classes, inline styles, utility classes (e.g. Tailwind), CSS-in-JS (e.g. Styled Components), etc. By default, all components include a builtin `className` attribute which can be targeted using CSS selectors. These follow the `react-aria-ComponentName` naming convention. .react-aria-Tabs { /* ... */ } .react-aria-Tabs { /* ... */ } .react-aria-Tabs { /* ... */ } A custom `className` can also be specified on any component. This overrides the default `className` provided by React Aria with your own. <Tabs className="my-tabs"> {/* ... */} </Tabs> <Tabs className="my-tabs"> {/* ... */} </Tabs> <Tabs className="my-tabs"> {/* ... */} </Tabs> In addition, some components support multiple UI states (e.g. pressed, hovered, etc.). React Aria components expose states using data attributes, which you can target in CSS selectors. For example: .react-aria-Tab[data-selected] { /* ... */ } .react-aria-Tab[data-focus-visible] { /* ... */ } .react-aria-Tab[data-selected] { /* ... */ } .react-aria-Tab[data-focus-visible] { /* ... */ } .react-aria-Tab[data-selected] { /* ... */ } .react-aria-Tab[data-focus-visible] { /* ... */ } The `className` and `style` props also accept functions which receive states for styling. This lets you dynamically determine the classes or styles to apply, which is useful when using utility CSS libraries like Tailwind. <Tab className={({ isSelected }) => isSelected ? 'bg-blue-400' : 'bg-gray-100'} > Settings </Tab> <Tab className={({ isSelected }) => isSelected ? 'bg-blue-400' : 'bg-gray-100'} > Settings </Tab> <Tab className={( { isSelected } ) => isSelected ? 'bg-blue-400' : 'bg-gray-100'} > Settings </Tab> Render props may also be used as children to alter what elements are rendered based on the current state. For example, you could render an extra element when an item is selected. <Tab> {({isSelected}) => ( <> {isSelected && <SelectionIndicator />} Item </> )} </Tab> <Tab> {({isSelected}) => ( <> {isSelected && <SelectionIndicator />} Item </> )} </Tab> <Tab> {( { isSelected } ) => ( <> {isSelected && ( <SelectionIndicator /> )} Item </> )} </Tab> The states and selectors for each component used in `Tabs` are documented below. ### Tabs# `Tabs` can be targeted with the `.react-aria-Tabs` CSS selector, or by overriding with a custom `className`. It supports the following states and render props: | Name | CSS Selector | Description | | --- | --- | --- | | `orientation` | `[data-orientation="horizontal | vertical"]` | The orientation of the tabs. | ### TabList# A `TabList` can be targeted with the `.react-aria-TabList` CSS selector, or by overriding with a custom `className`. It supports the following states: | Name | CSS Selector | Description | | --- | --- | --- | | `orientation` | `[data-orientation="horizontal | vertical"]` | The orientation of the tab list. | | `state` | `—` | State of the tab list. | ### Tab# A `Tab` can be targeted with the `.react-aria-Tab` CSS selector, or by overriding with a custom `className`. It supports the following states and render props: | Name | CSS Selector | Description | | --- | --- | --- | | `isHovered` | `[data-hovered]` | Whether the tab is currently hovered with a mouse. | | `isPressed` | `[data-pressed]` | Whether the tab is currently in a pressed state. | | `isSelected` | `[data-selected]` | Whether the tab is currently selected. | | `isFocused` | `[data-focused]` | Whether the tab is currently focused. | | `isFocusVisible` | `[data-focus-visible]` | Whether the tab is currently keyboard focused. | | `isDisabled` | `[data-disabled]` | Whether the tab is disabled. | ### TabPanel# A `TabPanel` can be targeted with the `.react-aria-TabPanel` CSS selector, or by overriding with a custom `className`. It supports the following states and render props: | Name | CSS Selector | Description | | --- | --- | --- | | `isFocused` | `[data-focused]` | Whether the tab panel is currently focused. | | `isFocusVisible` | `[data-focus-visible]` | Whether the tab panel is currently keyboard focused. | | `isInert` | `[data-inert]` | Whether the tab panel is currently non-interactive. This occurs when the `shouldForceMount` prop is true, and the corresponding tab is not selected. | | `state` | `—` | State of the tab list. | ## Advanced customization# * * * ### Contexts# All React Aria Components export a corresponding context that can be used to send props to them from a parent element. This enables you to build your own compositional APIs similar to those found in React Aria Components itself. You can send any prop or ref via context that you could pass to the corresponding component. The local props and ref on the component are merged with the ones passed via context, with the local props taking precedence (following the rules documented in mergeProps). | Component | Context | Props | Ref | | --- | --- | --- | --- | | `Tabs` | `TabsContext` | ` TabsProps ` | `HTMLDivElement` | This example shows a `Router` component that accepts `Tabs` and `Link` elements as children. When a link is clicked, it updates the selected tab accordingly. import type {PressEvent} from 'react-aria-components'; import {TabsContext, LinkContext} from 'react-aria-components'; function Router({children}) { let [selectedKey, onSelectionChange] = React.useState(null); let onPress = (e: PressEvent) => { onSelectionChange(e.target.getAttribute('data-href')); }; return ( <TabsContext.Provider value={{selectedKey, onSelectionChange}}> <LinkContext.Provider value={{onPress}}> {children} </LinkContext.Provider> </TabsContext.Provider> ); } import type {PressEvent} from 'react-aria-components'; import { LinkContext, TabsContext } from 'react-aria-components'; function Router({ children }) { let [selectedKey, onSelectionChange] = React.useState( null ); let onPress = (e: PressEvent) => { onSelectionChange(e.target.getAttribute('data-href')); }; return ( <TabsContext.Provider value={{ selectedKey, onSelectionChange }} > <LinkContext.Provider value={{ onPress }}> {children} </LinkContext.Provider> </TabsContext.Provider> ); } import type {PressEvent} from 'react-aria-components'; import { LinkContext, TabsContext } from 'react-aria-components'; function Router( { children } ) { let [ selectedKey, onSelectionChange ] = React.useState( null ); let onPress = ( e: PressEvent ) => { onSelectionChange( e.target .getAttribute( 'data-href' ) ); }; return ( <TabsContext.Provider value={{ selectedKey, onSelectionChange }} > <LinkContext.Provider value={{ onPress }} > {children} </LinkContext.Provider> </TabsContext.Provider> ); } Now clicking a link rendered within a `Router` navigates to the linked tab. import {Link} from 'react-aria-components'; <Router> <Tabs> <TabList aria-label="Mesozoic time periods"> <Tab id="triassic">Triassic</Tab> <Tab id="jurassic">Jurassic</Tab> <Tab id="cretaceous">Cretaceous</Tab> </TabList> <TabPanel id="triassic"> The Triassic ranges roughly from 252 million to 201 million years ago, preceding the <Link data-href="jurassic">Jurassic Period</Link>. </TabPanel> <TabPanel id="jurassic"> The Jurassic ranges from 200 million years to 145 million years ago, preceding the <Link data-href="cretaceous">Cretaceous Period</Link>. </TabPanel> <TabPanel id="cretaceous"> The Cretaceous is the longest period of the Mesozoic, spanning from 145 million to 66 million years ago. </TabPanel> </Tabs> </Router> import {Link} from 'react-aria-components'; <Router> <Tabs> <TabList aria-label="Mesozoic time periods"> <Tab id="triassic">Triassic</Tab> <Tab id="jurassic">Jurassic</Tab> <Tab id="cretaceous">Cretaceous</Tab> </TabList> <TabPanel id="triassic"> The Triassic ranges roughly from 252 million to 201 million years ago, preceding the{' '} <Link data-href="jurassic">Jurassic Period</Link>. </TabPanel> <TabPanel id="jurassic"> The Jurassic ranges from 200 million years to 145 million years ago, preceding the{' '} <Link data-href="cretaceous"> Cretaceous Period </Link>. </TabPanel> <TabPanel id="cretaceous"> The Cretaceous is the longest period of the Mesozoic, spanning from 145 million to 66 million years ago. </TabPanel> </Tabs> </Router> import {Link} from 'react-aria-components'; <Router> <Tabs> <TabList aria-label="Mesozoic time periods"> <Tab id="triassic"> Triassic </Tab> <Tab id="jurassic"> Jurassic </Tab> <Tab id="cretaceous"> Cretaceous </Tab> </TabList> <TabPanel id="triassic"> The Triassic ranges roughly from 252 million to 201 million years ago, preceding the {' '} <Link data-href="jurassic"> Jurassic Period </Link>. </TabPanel> <TabPanel id="jurassic"> The Jurassic ranges from 200 million years to 145 million years ago, preceding the{' '} <Link data-href="cretaceous"> Cretaceous Period </Link>. </TabPanel> <TabPanel id="cretaceous"> The Cretaceous is the longest period of the Mesozoic, spanning from 145 million to 66 million years ago. </TabPanel> </Tabs> </Router> Triassic Jurassic Cretaceous The Triassic ranges roughly from 252 million to 201 million years ago, preceding the Jurassic Period. ### State# Tabs provides a `TabListState` object to its children via `TabListStateContext`. This can be used to access and manipulate the tab list state. This example shows a `TabNavigation` component that can be placed within `Tabs` to navigate to the previous or next selected tab. import {TabListStateContext, Button} from 'react-aria-components'; function TabNavigation() { let state = React.useContext(TabListStateContext); let prevKey = state?.collection.getKeyBefore(state.selectedKey); let nextKey = state?.collection.getKeyAfter(state.selectedKey); let onPrev = prevKey != null ? () => state.setSelectedKey(prevKey) : null; let onNext = nextKey != null ? () => state.setSelectedKey(nextKey) : null; return ( <div className="button-group"> <Button aria-label="Previous tab" onPress={onPrev}>←</Button> <Button aria-label="Next tab" onPress={onNext}>→</Button> </div> ); } <Tabs> <div style={{display: 'flex'}}> <TabList aria-label="Tabs" style={{flex: 1}}> <Tab id="home">Home</Tab> <Tab id="projects">Projects</Tab> <Tab id="search">Search</Tab> </TabList> <TabNavigation /> </div> <TabPanel id="home">Home</TabPanel> <TabPanel id="projects">Projects</TabPanel> <TabPanel id="search">Search</TabPanel> </Tabs> import { Button, TabListStateContext } from 'react-aria-components'; function TabNavigation() { let state = React.useContext(TabListStateContext); let prevKey = state?.collection.getKeyBefore( state.selectedKey ); let nextKey = state?.collection.getKeyAfter( state.selectedKey ); let onPrev = prevKey != null ? () => state.setSelectedKey(prevKey) : null; let onNext = nextKey != null ? () => state.setSelectedKey(nextKey) : null; return ( <div className="button-group"> <Button aria-label="Previous tab" onPress={onPrev}> ← </Button> <Button aria-label="Next tab" onPress={onNext}> → </Button> </div> ); } <Tabs> <div style={{ display: 'flex' }}> <TabList aria-label="Tabs" style={{ flex: 1 }}> <Tab id="home">Home</Tab> <Tab id="projects">Projects</Tab> <Tab id="search">Search</Tab> </TabList> <TabNavigation /> </div> <TabPanel id="home">Home</TabPanel> <TabPanel id="projects">Projects</TabPanel> <TabPanel id="search">Search</TabPanel> </Tabs> import { Button, TabListStateContext } from 'react-aria-components'; function TabNavigation() { let state = React .useContext( TabListStateContext ); let prevKey = state ?.collection .getKeyBefore( state.selectedKey ); let nextKey = state ?.collection .getKeyAfter( state.selectedKey ); let onPrev = prevKey != null ? () => state .setSelectedKey( prevKey ) : null; let onNext = nextKey != null ? () => state .setSelectedKey( nextKey ) : null; return ( <div className="button-group"> <Button aria-label="Previous tab" onPress={onPrev} > ← </Button> <Button aria-label="Next tab" onPress={onNext} > → </Button> </div> ); } <Tabs> <div style={{ display: 'flex' }} > <TabList aria-label="Tabs" style={{ flex: 1 }} > <Tab id="home"> Home </Tab> <Tab id="projects"> Projects </Tab> <Tab id="search"> Search </Tab> </TabList> <TabNavigation /> </div> <TabPanel id="home"> Home </TabPanel> <TabPanel id="projects"> Projects </TabPanel> <TabPanel id="search"> Search </TabPanel> </Tabs> Home Projects Search ←→ Home ### Hooks# If you need to customize things even further, such as accessing internal state or customizing DOM structure, you can drop down to the lower level Hook-based API. See useTabList for more details. ## Testing# * * * ### Test utils alpha# `@react-aria/test-utils` offers common tabs interaction utilities which you may find helpful when writing tests. See here for more information on how to setup these utilities in your tests. Below is the full definition of the tabs tester and a sample of how you could use it in your test suite. // Tabs.test.ts import {render} from '@testing-library/react'; import {User} from '@react-aria/test-utils'; let testUtilUser = new User({ interactionType: 'mouse' }); // ... it('Tabs can change selection via keyboard', async function () { // Render your test component/app and initialize the listbox tester let { getByTestId } = render( <Tabs data-testid="test-tabs"> ... </Tabs> ); let tabsTester = testUtilUser.createTester('Tabs', { root: getByTestId('test-tabs'), interactionType: 'keyboard' }); let tabs = tabsTester.tabs; expect(tabsTester.selectedTab).toBe(tabs[0]); await tabsTester.triggerTab({ tab: 1 }); expect(tabsTester.selectedTab).toBe(tabs[1]); }); // Tabs.test.ts import {render} from '@testing-library/react'; import {User} from '@react-aria/test-utils'; let testUtilUser = new User({ interactionType: 'mouse' }); // ... it('Tabs can change selection via keyboard', async function () { // Render your test component/app and initialize the listbox tester let { getByTestId } = render( <Tabs data-testid="test-tabs"> ... </Tabs> ); let tabsTester = testUtilUser.createTester('Tabs', { root: getByTestId('test-tabs'), interactionType: 'keyboard' }); let tabs = tabsTester.tabs; expect(tabsTester.selectedTab).toBe(tabs[0]); await tabsTester.triggerTab({ tab: 1 }); expect(tabsTester.selectedTab).toBe(tabs[1]); }); // Tabs.test.ts import {render} from '@testing-library/react'; import {User} from '@react-aria/test-utils'; let testUtilUser = new User({ interactionType: 'mouse' }); // ... it('Tabs can change selection via keyboard', async function () { // Render your test component/app and initialize the listbox tester let { getByTestId } = render( <Tabs data-testid="test-tabs"> ... </Tabs> ); let tabsTester = testUtilUser .createTester( 'Tabs', { root: getByTestId( 'test-tabs' ), interactionType: 'keyboard' } ); let tabs = tabsTester.tabs; expect( tabsTester .selectedTab ).toBe(tabs[0]); await tabsTester .triggerTab({ tab: 1 }); expect( tabsTester .selectedTab ).toBe(tabs[1]); }); ### Properties | Name | Type | Description | | --- | --- | --- | | `tablist` | `HTMLElement` | Returns the tablist. | | `tabpanels` | `HTMLElement[]` | Returns the tabpanels. | | `tabs` | `HTMLElement[]` | Returns the tabs in the tablist. | | `selectedTab` | `HTMLElement | null` | Returns the currently selected tab in the tablist if any. | | `activeTabpanel` | `HTMLElement | null` | Returns the currently active tabpanel if any. | ### Methods | Method | Description | | --- | --- | | `constructor( (opts: TabsTesterOpts )): void` | | | `setInteractionType( (type: UserOpts ['interactionType'] )): void` | Set the interaction type used by the tabs tester. | | `findTab( (opts: { tabIndexOrText: number | | string } )): HTMLElement` | Returns a tab matching the specified index or text content. | | `triggerTab( (opts: TriggerTabOptions )): Promise<void>` | Triggers the specified tab. Defaults to using the interaction type set on the tabs tester. | --- ## Page: https://react-spectrum.adobe.com/react-aria/Dialog.html # Dialog A dialog is an overlay shown above other content in an application. <table><tbody><tr><th>install</th><td><code>yarn add react-aria-components</code></td></tr><tr><th>version</th><td>1.10.1</td></tr><tr><th>usage</th><td><code><span>import</span> {Dialog} <span>from</span> <span>'react-aria-components'</span></code></td></tr></tbody></table> View ARIA pattern W3C View repository GitHub View package NPM ## Example# * * * import {Button, Dialog, DialogTrigger, Heading, Input, Label, Modal, TextField} from 'react-aria-components'; <DialogTrigger> <Button>Sign up…</Button> <Modal> <Dialog> <form> <Heading slot="title">Sign up</Heading> <TextField autoFocus> <Label>First Name</Label> <Input /> </TextField> <TextField> <Label>Last Name</Label> <Input /> </TextField> <Button slot="close" style={{ marginTop: 8 }}> Submit </Button> </form> </Dialog> </Modal> </DialogTrigger> import { Button, Dialog, DialogTrigger, Heading, Input, Label, Modal, TextField } from 'react-aria-components'; <DialogTrigger> <Button>Sign up…</Button> <Modal> <Dialog> <form> <Heading slot="title">Sign up</Heading> <TextField autoFocus> <Label>First Name</Label> <Input /> </TextField> <TextField> <Label>Last Name</Label> <Input /> </TextField> <Button slot="close" style={{ marginTop: 8 }}> Submit </Button> </form> </Dialog> </Modal> </DialogTrigger> import { Button, Dialog, DialogTrigger, Heading, Input, Label, Modal, TextField } from 'react-aria-components'; <DialogTrigger> <Button> Sign up… </Button> <Modal> <Dialog> <form> <Heading slot="title"> Sign up </Heading> <TextField autoFocus > <Label> First Name </Label> <Input /> </TextField> <TextField> <Label> Last Name </Label> <Input /> </TextField> <Button slot="close" style={{ marginTop: 8 }} > Submit </Button> </form> </Dialog> </Modal> </DialogTrigger> Sign up… Show CSS .react-aria-Dialog { outline: none; padding: 30px; max-height: inherit; box-sizing: border-box; overflow: auto; .react-aria-Heading[slot=title] { line-height: 1em; margin-top: 0; } } .react-aria-Dialog { outline: none; padding: 30px; max-height: inherit; box-sizing: border-box; overflow: auto; .react-aria-Heading[slot=title] { line-height: 1em; margin-top: 0; } } .react-aria-Dialog { outline: none; padding: 30px; max-height: inherit; box-sizing: border-box; overflow: auto; .react-aria-Heading[slot=title] { line-height: 1em; margin-top: 0; } } ## Features# * * * The HTML <dialog> element can be used to build dialogs. However, it is not yet widely supported across browsers, and building fully accessible custom dialogs from scratch is very difficult and error prone. `Dialog` helps achieve accessible dialogs that can be styled as needed. * **Flexible** – Dialogs can be used within a Modal or Popover to create many types of overlay elements. * **Accessible** – Exposed to assistive technology as a `dialog` or `alertdialog` with ARIA. The dialog is automatically labeled by a nested `<Heading>` element. Content outside the dialog is hidden from assistive technologies while it is open. * **Focus management** – Focus is moved into the dialog on mount, and restored to the trigger element on unmount. While open, focus is contained within the dialog, preventing the user from tabbing outside. ## Anatomy# * * * A dialog consists of a container element and an optional title and close button. It can be placed within a Modal or Popover, to create modal dialogs, popovers, and other types of overlays. A `DialogTrigger` can be used to open a dialog overlay in response to a user action, e.g. clicking a button. import {Button, Dialog, DialogTrigger, Heading, Modal} from 'react-aria-components'; <DialogTrigger> <Button /> <Modal> <Dialog> <Heading slot="title" /> <Button slot="close" /> </Dialog> </Modal> </DialogTrigger> import { Button, Dialog, DialogTrigger, Heading, Modal } from 'react-aria-components'; <DialogTrigger> <Button /> <Modal> <Dialog> <Heading slot="title" /> <Button slot="close" /> </Dialog> </Modal> </DialogTrigger> import { Button, Dialog, DialogTrigger, Heading, Modal } from 'react-aria-components'; <DialogTrigger> <Button /> <Modal> <Dialog> <Heading slot="title" /> <Button slot="close" /> </Dialog> </Modal> </DialogTrigger> If a dialog does not have a visible heading element, an `aria-label` or `aria-labelledby` prop must be passed instead to identify the element to assistive technology. ## Examples# * * * Account Menu A Menu with an interactive header, built with a Dialog and Popover. Destructive Alert Dialog An animated confirmation dialog, styled with Tailwind CSS. Gesture Driven Modal Sheet An iOS-style gesture driven modal sheet built with Framer Motion. Notifications Popover A notifications popover styled with Tailwind CSS. ## Starter kits# * * * To help kick-start your project, we offer starter kits that include example implementations of all React Aria components with various styling solutions. All components are fully styled, including support for dark mode, high contrast mode, and all UI states. Each starter comes with a pre-configured Storybook that you can experiment with, or use as a starting point for your own component library. Vanilla CSS Download ZIP Preview Tailwind CSS Download ZIP Preview ## Popover# * * * A `Dialog` may be placed within a Popover to display it in context with a trigger element. import {OverlayArrow, Popover} from 'react-aria-components'; <DialogTrigger> <Button aria-label="Help">ⓘ</Button> <Popover> <OverlayArrow> <svg width={12} height={12} viewBox="0 0 12 12"> <path d="M0 0 L6 6 L12 0" /> </svg> </OverlayArrow> <Dialog> <Heading slot="title">Help</Heading> <p>For help accessing your account, please contact support.</p> </Dialog> </Popover> </DialogTrigger> import {OverlayArrow, Popover} from 'react-aria-components'; <DialogTrigger> <Button aria-label="Help">ⓘ</Button> <Popover> <OverlayArrow> <svg width={12} height={12} viewBox="0 0 12 12"> <path d="M0 0 L6 6 L12 0" /> </svg> </OverlayArrow> <Dialog> <Heading slot="title">Help</Heading> <p> For help accessing your account, please contact support. </p> </Dialog> </Popover> </DialogTrigger> import { OverlayArrow, Popover } from 'react-aria-components'; <DialogTrigger> <Button aria-label="Help"> ⓘ </Button> <Popover> <OverlayArrow> <svg width={12} height={12} viewBox="0 0 12 12" > <path d="M0 0 L6 6 L12 0" /> </svg> </OverlayArrow> <Dialog> <Heading slot="title"> Help </Heading> <p> For help accessing your account, please contact support. </p> </Dialog> </Popover> </DialogTrigger> ⓘ ## Alert dialog# * * * Alert dialogs are a special type of dialog meant to present a prompt that the user must confirm before an action proceeds. An alert dialog may also behave differently with assistive technologies, such as playing a system alert sound when opening. Use the `role="alertdialog"` prop on the `<Dialog>` element to make an alert dialog. <DialogTrigger> <Button>Delete…</Button> <Modal> <Dialog role="alertdialog"> {({close}) => ( <> <Heading slot="title">Delete file</Heading> <p>This will permanently delete the selected file. Continue?</p> <div style={{display: 'flex', gap: 8}}> <Button onPress={close}>Cancel</Button> <Button onPress={close}>Delete</Button> </div> </> )} </Dialog> </Modal> </DialogTrigger> <DialogTrigger> <Button>Delete…</Button> <Modal> <Dialog role="alertdialog"> {({ close }) => ( <> <Heading slot="title">Delete file</Heading> <p> This will permanently delete the selected file. Continue? </p> <div style={{ display: 'flex', gap: 8 }}> <Button onPress={close}>Cancel</Button> <Button onPress={close}>Delete</Button> </div> </> )} </Dialog> </Modal> </DialogTrigger> <DialogTrigger> <Button> Delete… </Button> <Modal> <Dialog role="alertdialog"> {({ close }) => ( <> <Heading slot="title"> Delete file </Heading> <p> This will permanently delete the selected file. Continue? </p> <div style={{ display: 'flex', gap: 8 }} > <Button onPress={close} > Cancel </Button> <Button onPress={close} > Delete </Button> </div> </> )} </Dialog> </Modal> </DialogTrigger> Delete… ## Custom trigger# * * * `DialogTrigger` works out of the box with any pressable React Aria component (e.g. Button, Link, etc.). Custom trigger elements such as third party components and other DOM elements are also supported by wrapping them with the `<Pressable>` component, or using the usePress hook. import {Pressable} from 'react-aria-components'; <DialogTrigger> <Pressable> <span role="button">Custom trigger</span> </Pressable> <Modal> <Dialog> <Heading slot="title">Dialog</Heading> <p>This dialog was triggered by a custom button.</p> <Button slot="close">Close</Button> </Dialog> </Modal> </DialogTrigger> import {Pressable} from 'react-aria-components'; <DialogTrigger> <Pressable> <span role="button">Custom trigger</span> </Pressable> <Modal> <Dialog> <Heading slot="title">Dialog</Heading> <p>This dialog was triggered by a custom button.</p> <Button slot="close">Close</Button> </Dialog> </Modal> </DialogTrigger> import {Pressable} from 'react-aria-components'; <DialogTrigger> <Pressable> <span role="button"> Custom trigger </span> </Pressable> <Modal> <Dialog> <Heading slot="title"> Dialog </Heading> <p> This dialog was triggered by a custom button. </p> <Button slot="close"> Close </Button> </Dialog> </Modal> </DialogTrigger> Custom trigger Note that any `<Pressable>` child must have an interactive ARIA role or use an appropriate semantic HTML element so that screen readers can announce the trigger. Trigger components must forward their `ref` and spread all props to a DOM element. const CustomTrigger = React.forwardRef((props, ref) => ( <button {...props} ref={ref} /> )); const CustomTrigger = React.forwardRef((props, ref) => ( <button {...props} ref={ref} /> )); const CustomTrigger = React.forwardRef(( props, ref ) => ( <button {...props} ref={ref} /> )); ## Props# * * * ### DialogTrigger# | Name | Type | Description | | --- | --- | --- | | `children` | `ReactNode` | | | `isOpen` | `boolean` | Whether the overlay is open by default (controlled). | | `defaultOpen` | `boolean` | Whether the overlay is open by default (uncontrolled). | Events | Name | Type | Description | | --- | --- | --- | | `onOpenChange` | `( (isOpen: boolean )) => void` | Handler that is called when the overlay's open state changes. | ### Dialog# | Name | Type | Description | | --- | --- | --- | | `children` | `ReactNode | ( (opts: DialogRenderProps )) => ReactNode` | Children of the dialog. A function may be provided to access a function to close the dialog. | | `className` | `string` | The CSS className for the element. | | `style` | `CSSProperties` | The inline style for the element. | Layout | Name | Type | Description | | --- | --- | --- | | `slot` | `string | null` | A slot name for the component. Slots allow the component to receive props from a parent component. An explicit `null` value indicates that the local props completely override all props received from a parent. | Accessibility | Name | Type | Default | Description | | --- | --- | --- | --- | | `role` | `'dialog' | 'alertdialog'` | `'dialog'` | The accessibility role for the dialog. | | `id` | `string` | — | The element's unique identifier. See MDN. | | `aria-label` | `string` | — | Defines a string value that labels the current element. | | `aria-labelledby` | `string` | — | Identifies the element (or elements) that labels the current element. | | `aria-describedby` | `string` | — | Identifies the element (or elements) that describes the object. | | `aria-details` | `string` | — | Identifies the element (or elements) that provide a detailed, extended description for the object. | ### Heading# A `<Heading>` accepts all HTML attributes. ### Button# A `<Button slot="close">` element can be placed inside a `<Dialog>` to close it when pressed. Show props | Name | Type | Default | Description | | --- | --- | --- | --- | | `form` | `string` | — | The `<form>` element to associate the button with. The value of this attribute must be the id of a `<form>` in the same document. | | `formAction` | `string` | — | The URL that processes the information submitted by the button. Overrides the action attribute of the button's form owner. | | `formEncType` | `string` | — | Indicates how to encode the form data that is submitted. | | `formMethod` | `string` | — | Indicates the HTTP method used to submit the form. | | `formNoValidate` | `boolean` | — | Indicates that the form is not to be validated when it is submitted. | | `formTarget` | `string` | — | Overrides the target attribute of the button's form owner. | | `name` | `string` | — | Submitted as a pair with the button's value as part of the form data. | | `value` | `string` | — | The value associated with the button's name when it's submitted with the form data. | | `isPending` | `boolean` | — | Whether the button is in a pending state. This disables press and hover events while retaining focusability, and announces the pending state to screen readers. | | `isDisabled` | `boolean` | — | Whether the button is disabled. | | `autoFocus` | `boolean` | — | Whether the element should receive focus on render. | | `type` | `'button' | 'submit' | 'reset'` | `'button'` | The behavior of the button when used in an HTML form. | | `children` | `ReactNode | ( (values: ButtonRenderProps & & { defaultChildren: ReactNode | | undefined } )) => ReactNode` | — | The children of the component. A function may be provided to alter the children based on component state. | | `className` | `string | ( (values: ButtonRenderProps & & { defaultClassName: string | | undefined } )) => string` | — | The CSS className for the element. A function may be provided to compute the class based on component state. | | `style` | `CSSProperties | ( (values: ButtonRenderProps & & { defaultStyle: CSSProperties } )) => CSSProperties | undefined` | — | The inline style for the element. A function may be provided to compute the style based on component state. | Events | Name | Type | Description | | --- | --- | --- | | `onPress` | `( (e: PressEvent )) => void` | Handler that is called when the press is released over the target. | | `onPressStart` | `( (e: PressEvent )) => void` | Handler that is called when a press interaction starts. | | `onPressEnd` | `( (e: PressEvent )) => void` | Handler that is called when a press interaction ends, either over the target or when the pointer leaves the target. | | `onPressChange` | `( (isPressed: boolean )) => void` | Handler that is called when the press state changes. | | `onPressUp` | `( (e: PressEvent )) => void` | Handler that is called when a press is released over the target, regardless of whether it started on the target or not. | | `onClick` | `( (e: MouseEvent<FocusableElement> )) => void` | **Not recommended – use `onPress` instead.** `onClick` is an alias for `onPress` provided for compatibility with other libraries. `onPress` provides additional event details for non-mouse interactions. | | `onFocus` | `( (e: FocusEvent<Target> )) => void` | Handler that is called when the element receives focus. | | `onBlur` | `( (e: FocusEvent<Target> )) => void` | Handler that is called when the element loses focus. | | `onFocusChange` | `( (isFocused: boolean )) => void` | Handler that is called when the element's focus status changes. | | `onKeyDown` | `( (e: KeyboardEvent )) => void` | Handler that is called when a key is pressed. | | `onKeyUp` | `( (e: KeyboardEvent )) => void` | Handler that is called when a key is released. | | `onHoverStart` | `( (e: HoverEvent )) => void` | Handler that is called when a hover interaction starts. | | `onHoverEnd` | `( (e: HoverEvent )) => void` | Handler that is called when a hover interaction ends. | | `onHoverChange` | `( (isHovering: boolean )) => void` | Handler that is called when the hover state changes. | Layout | Name | Type | Description | | --- | --- | --- | | `slot` | `string | null` | A slot name for the component. Slots allow the component to receive props from a parent component. An explicit `null` value indicates that the local props completely override all props received from a parent. | Accessibility | Name | Type | Description | | --- | --- | --- | | `id` | `string` | The element's unique identifier. See MDN. | | `excludeFromTabOrder` | `boolean` | Whether to exclude the element from the sequential tab order. If true, the element will not be focusable via the keyboard by tabbing. This should be avoided except in rare scenarios where an alternative means of accessing the element or its functionality via the keyboard is available. | | `preventFocusOnPress` | `boolean` | Whether to prevent focus from moving to the button when pressing it. Caution, this can make the button inaccessible and should only be used when alternative keyboard interaction is provided, such as ComboBox's MenuTrigger or a NumberField's increment/decrement control. | | `aria-expanded` | `boolean | 'true' | 'false'` | Indicates whether the element, or another grouping element it controls, is currently expanded or collapsed. | | `aria-haspopup` | `boolean | 'menu' | 'listbox' | 'tree' | 'grid' | 'dialog' | 'true' | 'false'` | Indicates the availability and type of interactive popup element, such as menu or dialog, that can be triggered by an element. | | `aria-controls` | `string` | Identifies the element (or elements) whose contents or presence are controlled by the current element. | | `aria-pressed` | `boolean | 'true' | 'false' | 'mixed'` | Indicates the current "pressed" state of toggle buttons. | | `aria-current` | `boolean | 'true' | 'false' | 'page' | 'step' | 'location' | 'date' | 'time'` | Indicates whether this element represents the current item within a container or set of related elements. | | `aria-label` | `string` | Defines a string value that labels the current element. | | `aria-labelledby` | `string` | Identifies the element (or elements) that labels the current element. | | `aria-describedby` | `string` | Identifies the element (or elements) that describes the object. | | `aria-details` | `string` | Identifies the element (or elements) that provide a detailed, extended description for the object. | ## Styling# * * * React Aria components can be styled in many ways, including using CSS classes, inline styles, utility classes (e.g. Tailwind), CSS-in-JS (e.g. Styled Components), etc. By default, all components include a builtin `className` attribute which can be targeted using CSS selectors. These follow the `react-aria-ComponentName` naming convention. .react-aria-Dialog { /* ... */ } .react-aria-Dialog { /* ... */ } .react-aria-Dialog { /* ... */ } A custom `className` can also be specified on any component. This overrides the default `className` provided by React Aria with your own. <Dialog className="my-dialog"> {/* ... */} </Dialog> <Dialog className="my-dialog"> {/* ... */} </Dialog> <Dialog className="my-dialog"> {/* ... */} </Dialog> The selectors for each component used in a `Dialog` are documented below. ### DialogTrigger# The `DialogTrigger` component does not render any DOM elements (it only passes through its children) so it does not support styling. If you need a wrapper element, add one yourself inside the `<DialogTrigger>`. <DialogTrigger> <div className="my-dialog-trigger"> {/* ... */} </div> </DialogTrigger> <DialogTrigger> <div className="my-dialog-trigger"> {/* ... */} </div> </DialogTrigger> <DialogTrigger> <div className="my-dialog-trigger"> {/* ... */} </div> </DialogTrigger> ### Dialog# A `Dialog` can be targeted with the `.react-aria-Dialog` CSS selector, or by overriding with a custom `className`. ### Heading# A `Heading` can be targeted with the `.react-aria-Heading` CSS selector, or by overriding with a custom `className`. ## Advanced customization# * * * ### Custom children# DialogTrigger and Dialog pass props to their child components, such as the trigger button and modal overlay, via their associated contexts. These contexts are exported so you can also consume them in your own custom components. This enables you to reuse existing components from your app or component library together with React Aria Components. | Component | Context | Props | Ref | | --- | --- | --- | --- | | `Button` | `ButtonContext` | ` ButtonProps ` | `HTMLButtonElement` | | `Dialog` | `DialogContext` | ` DialogProps ` | `HTMLElement` | | `Modal` | `ModalContext` | ` ModalOverlayProps ` | `HTMLDivElement` | | `Popover` | `PopoverContext` | ` PopoverProps ` | `HTMLElement` | | `Heading` | `HeadingContext` | ` HeadingProps ` | `HTMLHeadingElement` | This example consumes from `HeadingContext` in an existing styled heading component to make it compatible with React Aria Components. The `useContextProps` hook merges the local props and ref with the ones provided via context by Dialog. import type {HeadingProps} from 'react-aria-components'; import {HeadingContext, useContextProps} from 'react-aria-components'; const MyCustomHeading = React.forwardRef( (props: HeadingProps, ref: React.ForwardedRef<HTMLHeadingElement>) => { // Merge the local props and ref with the ones provided via context. [props, ref] = useContextProps(props, ref, HeadingContext); // ... your existing Heading component return <h2 {...props} ref={ref} />; } ); import type {HeadingProps} from 'react-aria-components'; import { HeadingContext, useContextProps } from 'react-aria-components'; const MyCustomHeading = React.forwardRef( ( props: HeadingProps, ref: React.ForwardedRef<HTMLHeadingElement> ) => { // Merge the local props and ref with the ones provided via context. [props, ref] = useContextProps( props, ref, HeadingContext ); // ... your existing Heading component return <h2 {...props} ref={ref} />; } ); import type {HeadingProps} from 'react-aria-components'; import { HeadingContext, useContextProps } from 'react-aria-components'; const MyCustomHeading = React.forwardRef( ( props: HeadingProps, ref: React.ForwardedRef< HTMLHeadingElement > ) => { // Merge the local props and ref with the ones provided via context. [props, ref] = useContextProps( props, ref, HeadingContext ); // ... your existing Heading component return ( <h2 {...props} ref={ref} /> ); } ); Now you can use `MyCustomHeading` within a `Dialog`, in place of the builtin React Aria Components `Heading`. <Dialog> <MyCustomHeading>Dialog title</MyCustomHeading> {/* ... */} </Dialog> <Dialog> <MyCustomHeading>Dialog title</MyCustomHeading> {/* ... */} </Dialog> <Dialog> <MyCustomHeading> Dialog title </MyCustomHeading> {/* ... */} </Dialog> ### State# DialogTrigger provides an `OverlayTriggerState` object to its children via `OverlayTriggerStateContext`. This can be used to access and manipulate the dialog trigger's state. This example shows a `CloseButton` component that can be placed within a `DialogTrigger` to close the overlay. import {OverlayTriggerStateContext} from 'react-aria-components'; function CloseButton() { let state = React.useContext(OverlayTriggerStateContext)!; return <Button onPress={() => state.close()}>Close</Button>; } <DialogTrigger> <Button>About</Button> <Modal isDismissable> <Dialog> <Heading slot="title">About</Heading> <p>Copyright © 2023 Adobe. All rights reserved.</p> <CloseButton /> </Dialog> </Modal> </DialogTrigger> import {OverlayTriggerStateContext} from 'react-aria-components'; function CloseButton() { let state = React.useContext(OverlayTriggerStateContext)!; return ( <Button onPress={() => state.close()}>Close</Button> ); } <DialogTrigger> <Button>About</Button> <Modal isDismissable> <Dialog> <Heading slot="title">About</Heading> <p>Copyright © 2023 Adobe. All rights reserved.</p> <CloseButton /> </Dialog> </Modal> </DialogTrigger> import {OverlayTriggerStateContext} from 'react-aria-components'; function CloseButton() { let state = React .useContext( OverlayTriggerStateContext )!; return ( <Button onPress={() => state.close()} > Close </Button> ); } <DialogTrigger> <Button> About </Button> <Modal isDismissable> <Dialog> <Heading slot="title"> About </Heading> <p> Copyright © 2023 Adobe. All rights reserved. </p> <CloseButton /> </Dialog> </Modal> </DialogTrigger> About ### Hooks# If you need to customize things further, such as accessing internal state or customizing DOM structure, you can drop down to the lower level Hook-based API. See useDialog for more details. --- ## Page: https://react-spectrum.adobe.com/react-aria/Modal.html # Modal A modal is an overlay element which blocks interaction with elements outside it. <table><tbody><tr><th>install</th><td><code>yarn add react-aria-components</code></td></tr><tr><th>version</th><td>1.10.1</td></tr><tr><th>usage</th><td><code><span>import</span> {Modal} <span>from</span> <span>'react-aria-components'</span></code></td></tr></tbody></table> View ARIA pattern W3C View repository GitHub View package NPM ## Example# * * * import {Button, Dialog, DialogTrigger, Heading, Input, Label, Modal, TextField} from 'react-aria-components'; <DialogTrigger> <Button>Sign up…</Button> <Modal> <Dialog> <form> <Heading slot="title">Sign up</Heading> <TextField autoFocus> <Label>First Name:</Label> <Input /> </TextField> <TextField> <Label>Last Name:</Label> <Input /> </TextField> <Button slot="close"> Submit </Button> </form> </Dialog> </Modal> </DialogTrigger> import { Button, Dialog, DialogTrigger, Heading, Input, Label, Modal, TextField } from 'react-aria-components'; <DialogTrigger> <Button>Sign up…</Button> <Modal> <Dialog> <form> <Heading slot="title">Sign up</Heading> <TextField autoFocus> <Label>First Name:</Label> <Input /> </TextField> <TextField> <Label>Last Name:</Label> <Input /> </TextField> <Button slot="close"> Submit </Button> </form> </Dialog> </Modal> </DialogTrigger> import { Button, Dialog, DialogTrigger, Heading, Input, Label, Modal, TextField } from 'react-aria-components'; <DialogTrigger> <Button> Sign up… </Button> <Modal> <Dialog> <form> <Heading slot="title"> Sign up </Heading> <TextField autoFocus > <Label> First Name: </Label> <Input /> </TextField> <TextField> <Label> Last Name: </Label> <Input /> </TextField> <Button slot="close"> Submit </Button> </form> </Dialog> </Modal> </DialogTrigger> Sign up… Show CSS @import "@react-aria/example-theme"; .react-aria-ModalOverlay { position: fixed; top: 0; left: 0; width: 100vw; height: var(--visual-viewport-height); background: rgba(0 0 0 / .5); display: flex; align-items: center; justify-content: center; z-index: 100; &[data-entering] { animation: modal-fade 200ms; } &[data-exiting] { animation: modal-fade 150ms reverse ease-in; } } .react-aria-Modal { box-shadow: 0 8px 20px rgba(0 0 0 / 0.1); border-radius: 6px; background: var(--overlay-background); color: var(--text-color); border: 1px solid var(--gray-400); outline: none; max-width: 300px; &[data-entering] { animation: modal-zoom 300ms cubic-bezier(0.175, 0.885, 0.32, 1.275); } .react-aria-TextField { margin-bottom: 8px; } } @keyframes modal-fade { from { opacity: 0; } to { opacity: 1; } } @keyframes modal-zoom { from { transform: scale(0.8); } to { transform: scale(1); } } @import "@react-aria/example-theme"; .react-aria-ModalOverlay { position: fixed; top: 0; left: 0; width: 100vw; height: var(--visual-viewport-height); background: rgba(0 0 0 / .5); display: flex; align-items: center; justify-content: center; z-index: 100; &[data-entering] { animation: modal-fade 200ms; } &[data-exiting] { animation: modal-fade 150ms reverse ease-in; } } .react-aria-Modal { box-shadow: 0 8px 20px rgba(0 0 0 / 0.1); border-radius: 6px; background: var(--overlay-background); color: var(--text-color); border: 1px solid var(--gray-400); outline: none; max-width: 300px; &[data-entering] { animation: modal-zoom 300ms cubic-bezier(0.175, 0.885, 0.32, 1.275); } .react-aria-TextField { margin-bottom: 8px; } } @keyframes modal-fade { from { opacity: 0; } to { opacity: 1; } } @keyframes modal-zoom { from { transform: scale(0.8); } to { transform: scale(1); } } @import "@react-aria/example-theme"; .react-aria-ModalOverlay { position: fixed; top: 0; left: 0; width: 100vw; height: var(--visual-viewport-height); background: rgba(0 0 0 / .5); display: flex; align-items: center; justify-content: center; z-index: 100; &[data-entering] { animation: modal-fade 200ms; } &[data-exiting] { animation: modal-fade 150ms reverse ease-in; } } .react-aria-Modal { box-shadow: 0 8px 20px rgba(0 0 0 / 0.1); border-radius: 6px; background: var(--overlay-background); color: var(--text-color); border: 1px solid var(--gray-400); outline: none; max-width: 300px; &[data-entering] { animation: modal-zoom 300ms cubic-bezier(0.175, 0.885, 0.32, 1.275); } .react-aria-TextField { margin-bottom: 8px; } } @keyframes modal-fade { from { opacity: 0; } to { opacity: 1; } } @keyframes modal-zoom { from { transform: scale(0.8); } to { transform: scale(1); } } ## Features# * * * The HTML <dialog> element can be used to build modals. However, it is not yet widely supported across browsers, and building fully accessible custom dialogs from scratch is very difficult and error prone. `Modal` helps achieve accessible modals that can be styled as needed. * **Styleable** – States for entry and exit animations are included for easy styling. Both the underlay and overlay elements can be customized. * **Accessible** – Content outside the model is hidden from assistive technologies while it is open. The modal optionally closes when interacting outside, or pressing the Escape key. * **Focus management** – Focus is moved into the modal on mount, and restored to the trigger element on unmount. While open, focus is contained within the modal, preventing the user from tabbing outside. * **Scroll locking** – Scrolling the page behind the modal is prevented while it is open, including in mobile browsers. Note: `Modal` only provides the overlay itself. It should be combined with Dialog to create fully accessible modal dialogs. Other overlays such as menus may also be placed in a modal overlay. ## Anatomy# * * * A modal consists of an overlay container element, and an underlay. The overlay may contain a Dialog, or another element such as a Menu or ListBox when used within a component such as a Select or ComboBox. The underlay is typically a partially transparent element that covers the rest of the screen behind the overlay, and prevents the user from interacting with the elements behind it. import {Modal, ModalOverlay} from 'react-aria-components'; <ModalOverlay> <Modal /> </ModalOverlay> import {Modal, ModalOverlay} from 'react-aria-components'; <ModalOverlay> <Modal /> </ModalOverlay> import { Modal, ModalOverlay } from 'react-aria-components'; <ModalOverlay> <Modal /> </ModalOverlay> ## Examples# * * * Destructive Alert Dialog An animated confirmation dialog, styled with Tailwind CSS. Gesture Driven Modal Sheet An iOS-style gesture driven modal sheet built with Framer Motion. ## Starter kits# * * * To help kick-start your project, we offer starter kits that include example implementations of all React Aria components with various styling solutions. All components are fully styled, including support for dark mode, high contrast mode, and all UI states. Each starter comes with a pre-configured Storybook that you can experiment with, or use as a starting point for your own component library. Vanilla CSS Download ZIP Preview Tailwind CSS Download ZIP Preview ## Interactions# * * * ### Dismissable# If your modal doesn't require the user to make a confirmation, you can set `isDismissable` on the `Modal`. This allows the user to click outside to close the dialog. <DialogTrigger> <Button>Open dialog</Button> <Modal isDismissable> <Dialog> <Heading slot="title">Notice</Heading> <p>Click outside to close this dialog.</p> </Dialog> </Modal> </DialogTrigger> <DialogTrigger> <Button>Open dialog</Button> <Modal isDismissable> <Dialog> <Heading slot="title">Notice</Heading> <p>Click outside to close this dialog.</p> </Dialog> </Modal> </DialogTrigger> <DialogTrigger> <Button> Open dialog </Button> <Modal isDismissable> <Dialog> <Heading slot="title"> Notice </Heading> <p> Click outside to close this dialog. </p> </Dialog> </Modal> </DialogTrigger> Open dialog ### Keyboard dismiss disabled# By default, modals can be closed by pressing the Escape key. This can be disabled with the `isKeyboardDismissDisabled` prop. <DialogTrigger> <Button>Open dialog</Button> <Modal isKeyboardDismissDisabled> <Dialog> <Heading slot="title">Notice</Heading> <p>You must close this dialog using the button below.</p> <Button slot="close">Close</Button> </Dialog> </Modal> </DialogTrigger> <DialogTrigger> <Button>Open dialog</Button> <Modal isKeyboardDismissDisabled> <Dialog> <Heading slot="title">Notice</Heading> <p> You must close this dialog using the button below. </p> <Button slot="close">Close</Button> </Dialog> </Modal> </DialogTrigger> <DialogTrigger> <Button> Open dialog </Button> <Modal isKeyboardDismissDisabled > <Dialog> <Heading slot="title"> Notice </Heading> <p> You must close this dialog using the button below. </p> <Button slot="close"> Close </Button> </Dialog> </Modal> </DialogTrigger> Open dialog ## Custom overlay# * * * `ModalOverlay` can be used to customize the backdrop rendered behind a `Modal`. Together with support for custom entry and exit animations, you can build other types of overlays beyond traditional modal dialogs such as trays or drawers. import {ModalOverlay} from 'react-aria-components'; <DialogTrigger> <Button>Open modal</Button> <ModalOverlay className="my-overlay"> <Modal className="my-modal"> <Dialog> <Heading slot="title">Notice</Heading> <p>This is a modal with a custom modal overlay.</p> <Button slot="close">Close</Button> </Dialog> </Modal> </ModalOverlay> </DialogTrigger> import {ModalOverlay} from 'react-aria-components'; <DialogTrigger> <Button>Open modal</Button> <ModalOverlay className="my-overlay"> <Modal className="my-modal"> <Dialog> <Heading slot="title">Notice</Heading> <p>This is a modal with a custom modal overlay.</p> <Button slot="close">Close</Button> </Dialog> </Modal> </ModalOverlay> </DialogTrigger> import {ModalOverlay} from 'react-aria-components'; <DialogTrigger> <Button> Open modal </Button> <ModalOverlay className="my-overlay"> <Modal className="my-modal"> <Dialog> <Heading slot="title"> Notice </Heading> <p> This is a modal with a custom modal overlay. </p> <Button slot="close"> Close </Button> </Dialog> </Modal> </ModalOverlay> </DialogTrigger> Open modal Show CSS .my-overlay { position: fixed; inset: 0; background: rgba(45 0 0 / .3); backdrop-filter: blur(10px); &[data-entering] { animation: mymodal-blur 300ms; } &[data-exiting] { animation: mymodal-blur 300ms reverse ease-in; } } .my-modal { position: fixed; top: 0; bottom: 0; right: 0; width: 300px; background: var(--overlay-background); outline: none; border-left: 1px solid var(--border-color); box-shadow: -8px 0 20px rgba(0 0 0 / 0.1); &[data-entering] { animation: mymodal-slide 300ms; } &[data-exiting] { animation: mymodal-slide 300ms reverse ease-in; } } @keyframes mymodal-blur { from { background: rgba(45 0 0 / 0); backdrop-filter: blur(0); } to { background: rgba(45 0 0 / .3); backdrop-filter: blur(10px); } } @keyframes mymodal-slide { from { transform: translateX(100%); } to { transform: translateX(0); } } .my-overlay { position: fixed; inset: 0; background: rgba(45 0 0 / .3); backdrop-filter: blur(10px); &[data-entering] { animation: mymodal-blur 300ms; } &[data-exiting] { animation: mymodal-blur 300ms reverse ease-in; } } .my-modal { position: fixed; top: 0; bottom: 0; right: 0; width: 300px; background: var(--overlay-background); outline: none; border-left: 1px solid var(--border-color); box-shadow: -8px 0 20px rgba(0 0 0 / 0.1); &[data-entering] { animation: mymodal-slide 300ms; } &[data-exiting] { animation: mymodal-slide 300ms reverse ease-in; } } @keyframes mymodal-blur { from { background: rgba(45 0 0 / 0); backdrop-filter: blur(0); } to { background: rgba(45 0 0 / .3); backdrop-filter: blur(10px); } } @keyframes mymodal-slide { from { transform: translateX(100%); } to { transform: translateX(0); } } .my-overlay { position: fixed; inset: 0; background: rgba(45 0 0 / .3); backdrop-filter: blur(10px); &[data-entering] { animation: mymodal-blur 300ms; } &[data-exiting] { animation: mymodal-blur 300ms reverse ease-in; } } .my-modal { position: fixed; top: 0; bottom: 0; right: 0; width: 300px; background: var(--overlay-background); outline: none; border-left: 1px solid var(--border-color); box-shadow: -8px 0 20px rgba(0 0 0 / 0.1); &[data-entering] { animation: mymodal-slide 300ms; } &[data-exiting] { animation: mymodal-slide 300ms reverse ease-in; } } @keyframes mymodal-blur { from { background: rgba(45 0 0 / 0); backdrop-filter: blur(0); } to { background: rgba(45 0 0 / .3); backdrop-filter: blur(10px); } } @keyframes mymodal-slide { from { transform: translateX(100%); } to { transform: translateX(0); } } ## Controlled open state# * * * The above examples have shown `Modal` used within a `<DialogTrigger>`, which handles opening the modal when a button is clicked. This is convenient, but there are cases where you want to show a modal programmatically rather than as a result of a user action, or render the `<Modal>` in a different part of the JSX tree. To do this, you can manage the modal's `isOpen` state yourself and provide it as a prop to the `<Modal>` element. The `onOpenChange` prop will be called when the user closes the modal, and should be used to update your state. function Example() { let [isOpen, setOpen] = React.useState(false); return ( <> <Button onPress={() => setOpen(true)}>Open dialog</Button> <Modal isDismissable isOpen={isOpen} onOpenChange={setOpen}> <Dialog> <Heading slot="title">Notice</Heading> <p>Click outside to close this dialog.</p> </Dialog> </Modal> </> ); } function Example() { let [isOpen, setOpen] = React.useState(false); return ( <> <Button onPress={() => setOpen(true)}> Open dialog </Button> <Modal isDismissable isOpen={isOpen} onOpenChange={setOpen} > <Dialog> <Heading slot="title">Notice</Heading> <p>Click outside to close this dialog.</p> </Dialog> </Modal> </> ); } function Example() { let [isOpen, setOpen] = React.useState( false ); return ( <> <Button onPress={() => setOpen(true)} > Open dialog </Button> <Modal isDismissable isOpen={isOpen} onOpenChange={setOpen} > <Dialog> <Heading slot="title"> Notice </Heading> <p> Click outside to close this dialog. </p> </Dialog> </Modal> </> ); } Open dialog ## Custom trigger# * * * `DialogTrigger` works out of the box with any pressable React Aria component (e.g. Button, Link, etc.). Custom trigger elements such as third party components and other DOM elements are also supported by wrapping them with the `<Pressable>` component, or using the usePress hook. import {Pressable} from 'react-aria-components'; <DialogTrigger> <Pressable> <span role="button">Custom trigger</span> </Pressable> <Modal> <Dialog> <Heading slot="title">Dialog</Heading> <p>This dialog was triggered by a custom button.</p> <Button slot="close">Close</Button> </Dialog> </Modal> </DialogTrigger> import {Pressable} from 'react-aria-components'; <DialogTrigger> <Pressable> <span role="button">Custom trigger</span> </Pressable> <Modal> <Dialog> <Heading slot="title">Dialog</Heading> <p>This dialog was triggered by a custom button.</p> <Button slot="close">Close</Button> </Dialog> </Modal> </DialogTrigger> import {Pressable} from 'react-aria-components'; <DialogTrigger> <Pressable> <span role="button"> Custom trigger </span> </Pressable> <Modal> <Dialog> <Heading slot="title"> Dialog </Heading> <p> This dialog was triggered by a custom button. </p> <Button slot="close"> Close </Button> </Dialog> </Modal> </DialogTrigger> Custom trigger Note that any `<Pressable>` child must have an interactive ARIA role or use an appropriate semantic HTML element so that screen readers can announce the trigger. Trigger components must forward their `ref` and spread all props to a DOM element. const CustomTrigger = React.forwardRef((props, ref) => ( <button {...props} ref={ref} /> )); const CustomTrigger = React.forwardRef((props, ref) => ( <button {...props} ref={ref} /> )); const CustomTrigger = React.forwardRef(( props, ref ) => ( <button {...props} ref={ref} /> )); ## Props# * * * | Name | Type | Default | Description | | --- | --- | --- | --- | | `isEntering` | `boolean` | — | Whether the modal is currently performing an entry animation. | | `isExiting` | `boolean` | — | Whether the modal is currently performing an exit animation. | | `isDismissable` | `boolean` | `false` | Whether to close the modal when the user interacts outside it. | | `isKeyboardDismissDisabled` | `boolean` | `false` | Whether pressing the escape key to close the modal should be disabled. | | `shouldCloseOnInteractOutside` | `( (element: Element )) => boolean` | — | When user interacts with the argument element outside of the overlay ref, return true if onClose should be called. This gives you a chance to filter out interaction with elements that should not dismiss the overlay. By default, onClose will always be called on interaction outside the overlay ref. | | `isOpen` | `boolean` | — | Whether the overlay is open by default (controlled). | | `defaultOpen` | `boolean` | — | Whether the overlay is open by default (uncontrolled). | | `children` | `ReactNode | ( (values: ModalRenderProps & & { defaultChildren: ReactNode | | undefined } )) => ReactNode` | — | The children of the component. A function may be provided to alter the children based on component state. | | `className` | `string | ( (values: ModalRenderProps & & { defaultClassName: string | | undefined } )) => string` | — | The CSS className for the element. A function may be provided to compute the class based on component state. | | `style` | `CSSProperties | ( (values: ModalRenderProps & & { defaultStyle: CSSProperties } )) => CSSProperties | undefined` | — | The inline style for the element. A function may be provided to compute the style based on component state. | Events | Name | Type | Description | | --- | --- | --- | | `onOpenChange` | `( (isOpen: boolean )) => void` | Handler that is called when the overlay's open state changes. | Layout | Name | Type | Description | | --- | --- | --- | | `slot` | `string | null` | A slot name for the component. Slots allow the component to receive props from a parent component. An explicit `null` value indicates that the local props completely override all props received from a parent. | ## Styling# * * * React Aria components can be styled in many ways, including using CSS classes, inline styles, utility classes (e.g. Tailwind), CSS-in-JS (e.g. Styled Components), etc. By default, all components include a builtin `className` attribute which can be targeted using CSS selectors. These follow the `react-aria-ComponentName` naming convention. .react-aria-Modal { /* ... */ } .react-aria-Modal { /* ... */ } .react-aria-Modal { /* ... */ } A custom `className` can also be specified on any component. This overrides the default `className` provided by React Aria with your own. <Modal className="my-modal"> {/* ... */} </Modal> <Modal className="my-modal"> {/* ... */} </Modal> <Modal className="my-modal"> {/* ... */} </Modal> In addition, modals support entry and exit animations, which are exposed as states using DOM attributes that you can target with CSS selectors. `Modal` and `ModalOverlay` will automatically wait for any exit animations to complete before removing the element from the DOM. See the animation guide for more details. .react-aria-Modal[data-entering] { animation: slide 300ms; } .react-aria-Modal[data-exiting] { animation: slide 300ms reverse; } @keyframes slide { /* ... */ } .react-aria-Modal[data-entering] { animation: slide 300ms; } .react-aria-Modal[data-exiting] { animation: slide 300ms reverse; } @keyframes slide { /* ... */ } .react-aria-Modal[data-entering] { animation: slide 300ms; } .react-aria-Modal[data-exiting] { animation: slide 300ms reverse; } @keyframes slide { /* ... */ } The `className` and `style` props also accept functions which receive states for styling. This lets you dynamically determine the classes or styles to apply, which is useful when using utility CSS libraries like Tailwind. <Modal className={({isEntering}) => isEntering ? 'slide-in' : ''}> {/* ... */} </Modal> <Modal className={({ isEntering }) => isEntering ? 'slide-in' : ''} > {/* ... */} </Modal> <Modal className={( { isEntering } ) => isEntering ? 'slide-in' : ''} > {/* ... */} </Modal> The states, selectors, and render props for each component used in a `Modal` are documented below. ### Modal# A `Modal` can be targeted with the `.react-aria-Modal` CSS selector, or by overriding with a custom `className`. It supports the following states and render props: | Name | CSS Selector | Description | | --- | --- | --- | | `isEntering` | `[data-entering]` | Whether the modal is currently entering. Use this to apply animations. | | `isExiting` | `[data-exiting]` | Whether the modal is currently exiting. Use this to apply animations. | | `state` | `—` | State of the modal. | ### ModalOverlay# By default, `Modal` includes a builtin `ModalOverlay`, which renders a backdrop over the page when a modal is open. This can be targeted using the `.react-aria-ModalOverlay` CSS selector. To customize the `ModalOverlay` with a different class name or other attributes, render a `ModalOverlay` and place a `Modal` inside. The `--visual-viewport-height` CSS custom property will be set on the `ModalOverlay`, which you can use to set the height to account for the virtual keyboard on mobile. .react-aria-ModalOverlay { position: fixed; height: var(--visual-viewport-height); } .react-aria-ModalOverlay { position: fixed; height: var(--visual-viewport-height); } .react-aria-ModalOverlay { position: fixed; height: var(--visual-viewport-height); } ## Advanced customization# * * * ### Contexts# All React Aria Components export a corresponding context that can be used to send props to them from a parent element. This enables you to build your own compositional APIs similar to those found in React Aria Components itself. You can send any prop or ref via context that you could pass to the corresponding component. The local props and ref on the component are merged with the ones passed via context, with the local props taking precedence (following the rules documented in mergeProps). | Component | Context | Props | Ref | | --- | --- | --- | --- | | `Modal` | `ModalContext` | ` ModalOverlayProps ` | `HTMLDivElement` | This example shows a `KeyboardModalTrigger` component that shows a modal when a user presses a specific key from anywhere on the page. It uses `ModalContext` to set the open state of the nested modal. import {ModalContext} from 'react-aria-components'; interface KeyboardModalTriggerProps { keyboardShortcut: string, children: React.ReactNode } function KeyboardModalTrigger(props: KeyboardModalTriggerProps) { let [isOpen, setOpen] = React.useState(false); React.useEffect(() => { let onKeyDown = (e: KeyboardEvent) => { if (e.key === props.keyboardShortcut) { setOpen(true); } }; document.addEventListener('keydown', onKeyDown); return () => document.removeEventListener('keydown', onKeyDown); }, [props.keyboardShortcut]); return ( <ModalContext.Provider value={{isOpen, onOpenChange: setOpen}}> {props.children} </ModalContext.Provider> ); } import {ModalContext} from 'react-aria-components'; interface KeyboardModalTriggerProps { keyboardShortcut: string; children: React.ReactNode; } function KeyboardModalTrigger( props: KeyboardModalTriggerProps ) { let [isOpen, setOpen] = React.useState(false); React.useEffect(() => { let onKeyDown = (e: KeyboardEvent) => { if (e.key === props.keyboardShortcut) { setOpen(true); } }; document.addEventListener('keydown', onKeyDown); return () => document.removeEventListener('keydown', onKeyDown); }, [props.keyboardShortcut]); return ( <ModalContext.Provider value={{ isOpen, onOpenChange: setOpen }} > {props.children} </ModalContext.Provider> ); } import {ModalContext} from 'react-aria-components'; interface KeyboardModalTriggerProps { keyboardShortcut: string; children: React.ReactNode; } function KeyboardModalTrigger( props: KeyboardModalTriggerProps ) { let [isOpen, setOpen] = React.useState( false ); React.useEffect(() => { let onKeyDown = ( e: KeyboardEvent ) => { if ( e.key === props .keyboardShortcut ) { setOpen(true); } }; document .addEventListener( 'keydown', onKeyDown ); return () => document .removeEventListener( 'keydown', onKeyDown ); }, [ props .keyboardShortcut ]); return ( <ModalContext.Provider value={{ isOpen, onOpenChange: setOpen }} > {props.children} </ModalContext.Provider> ); } The following example uses `KeyboardModalTrigger` to show a modal when the / key is pressed. <KeyboardModalTrigger keyboardShortcut="/"> <Modal isDismissable> <Dialog> <Heading slot="title">Command palette</Heading> <p>Your cool command palette UI here!</p> </Dialog> </Modal> </KeyboardModalTrigger> <KeyboardModalTrigger keyboardShortcut="/"> <Modal isDismissable> <Dialog> <Heading slot="title">Command palette</Heading> <p>Your cool command palette UI here!</p> </Dialog> </Modal> </KeyboardModalTrigger> <KeyboardModalTrigger keyboardShortcut="/"> <Modal isDismissable> <Dialog> <Heading slot="title"> Command palette </Heading> <p> Your cool command palette UI here! </p> </Dialog> </Modal> </KeyboardModalTrigger> ### Hooks# If you need to customize things further, such as accessing internal state or customizing DOM structure, you can drop down to the lower level Hook-based API. See useModalOverlay for more details. --- ## Page: https://react-spectrum.adobe.com/react-aria/Popover.html # Popover A popover is an overlay element positioned relative to a trigger. <table><tbody><tr><th>install</th><td><code>yarn add react-aria-components</code></td></tr><tr><th>version</th><td>1.10.1</td></tr><tr><th>usage</th><td><code><span>import</span> {Popover} <span>from</span> <span>'react-aria-components'</span></code></td></tr></tbody></table> View ARIA pattern W3C View repository GitHub View package NPM ## Example# * * * import {Button, Dialog, DialogTrigger, Heading, OverlayArrow, Popover, Switch} from 'react-aria-components'; <DialogTrigger> <Button>Settings</Button> <Popover> <OverlayArrow> <svg width={12} height={12} viewBox="0 0 12 12"> <path d="M0 0 L6 6 L12 0" /> </svg> </OverlayArrow> <Dialog> <div className="flex-col"> <Switch defaultSelected> <div className="indicator" /> Wi-Fi </Switch> <Switch defaultSelected> <div className="indicator" /> Bluetooth </Switch> <Switch> <div className="indicator" /> Mute </Switch> </div> </Dialog> </Popover> </DialogTrigger> import { Button, Dialog, DialogTrigger, Heading, OverlayArrow, Popover, Switch } from 'react-aria-components'; <DialogTrigger> <Button>Settings</Button> <Popover> <OverlayArrow> <svg width={12} height={12} viewBox="0 0 12 12"> <path d="M0 0 L6 6 L12 0" /> </svg> </OverlayArrow> <Dialog> <div className="flex-col"> <Switch defaultSelected> <div className="indicator" /> Wi-Fi </Switch> <Switch defaultSelected> <div className="indicator" /> Bluetooth </Switch> <Switch> <div className="indicator" /> Mute </Switch> </div> </Dialog> </Popover> </DialogTrigger> import { Button, Dialog, DialogTrigger, Heading, OverlayArrow, Popover, Switch } from 'react-aria-components'; <DialogTrigger> <Button> Settings </Button> <Popover> <OverlayArrow> <svg width={12} height={12} viewBox="0 0 12 12" > <path d="M0 0 L6 6 L12 0" /> </svg> </OverlayArrow> <Dialog> <div className="flex-col"> <Switch defaultSelected > <div className="indicator" /> {' '} Wi-Fi </Switch> <Switch defaultSelected > <div className="indicator" /> {' '} Bluetooth </Switch> <Switch> <div className="indicator" /> {' '} Mute </Switch> </div> </Dialog> </Popover> </DialogTrigger> Settings Show CSS @import "@react-aria/example-theme"; .react-aria-Popover { --background-color: var(--overlay-background); border: 1px solid var(--border-color); box-shadow: 0 8px 20px rgba(0 0 0 / 0.1); border-radius: 6px; background: var(--background-color); color: var(--text-color); outline: none; max-width: 250px; transition: transform 200ms, opacity 200ms; .react-aria-OverlayArrow svg { display: block; fill: var(--background-color); stroke: var(--border-color); stroke-width: 1px; } &[data-entering], &[data-exiting] { transform: var(--origin); opacity: 0; } &[data-placement=top] { --origin: translateY(8px); &:has(.react-aria-OverlayArrow) { margin-bottom: 6px; } } &[data-placement=bottom] { --origin: translateY(-8px); &:has(.react-aria-OverlayArrow) { margin-top: 6px; } .react-aria-OverlayArrow svg { transform: rotate(180deg); } } &[data-placement=right] { --origin: translateX(-8px); &:has(.react-aria-OverlayArrow) { margin-left: 6px; } .react-aria-OverlayArrow svg { transform: rotate(90deg); } } &[data-placement=left] { --origin: translateX(8px); &:has(.react-aria-OverlayArrow) { margin-right: 6px; } .react-aria-OverlayArrow svg { transform: rotate(-90deg); } } } @import "@react-aria/example-theme"; .react-aria-Popover { --background-color: var(--overlay-background); border: 1px solid var(--border-color); box-shadow: 0 8px 20px rgba(0 0 0 / 0.1); border-radius: 6px; background: var(--background-color); color: var(--text-color); outline: none; max-width: 250px; transition: transform 200ms, opacity 200ms; .react-aria-OverlayArrow svg { display: block; fill: var(--background-color); stroke: var(--border-color); stroke-width: 1px; } &[data-entering], &[data-exiting] { transform: var(--origin); opacity: 0; } &[data-placement=top] { --origin: translateY(8px); &:has(.react-aria-OverlayArrow) { margin-bottom: 6px; } } &[data-placement=bottom] { --origin: translateY(-8px); &:has(.react-aria-OverlayArrow) { margin-top: 6px; } .react-aria-OverlayArrow svg { transform: rotate(180deg); } } &[data-placement=right] { --origin: translateX(-8px); &:has(.react-aria-OverlayArrow) { margin-left: 6px; } .react-aria-OverlayArrow svg { transform: rotate(90deg); } } &[data-placement=left] { --origin: translateX(8px); &:has(.react-aria-OverlayArrow) { margin-right: 6px; } .react-aria-OverlayArrow svg { transform: rotate(-90deg); } } } @import "@react-aria/example-theme"; .react-aria-Popover { --background-color: var(--overlay-background); border: 1px solid var(--border-color); box-shadow: 0 8px 20px rgba(0 0 0 / 0.1); border-radius: 6px; background: var(--background-color); color: var(--text-color); outline: none; max-width: 250px; transition: transform 200ms, opacity 200ms; .react-aria-OverlayArrow svg { display: block; fill: var(--background-color); stroke: var(--border-color); stroke-width: 1px; } &[data-entering], &[data-exiting] { transform: var(--origin); opacity: 0; } &[data-placement=top] { --origin: translateY(8px); &:has(.react-aria-OverlayArrow) { margin-bottom: 6px; } } &[data-placement=bottom] { --origin: translateY(-8px); &:has(.react-aria-OverlayArrow) { margin-top: 6px; } .react-aria-OverlayArrow svg { transform: rotate(180deg); } } &[data-placement=right] { --origin: translateX(-8px); &:has(.react-aria-OverlayArrow) { margin-left: 6px; } .react-aria-OverlayArrow svg { transform: rotate(90deg); } } &[data-placement=left] { --origin: translateX(8px); &:has(.react-aria-OverlayArrow) { margin-right: 6px; } .react-aria-OverlayArrow svg { transform: rotate(-90deg); } } } ## Features# * * * There is no built in way to create popovers in HTML. `Popover` helps achieve accessible popovers that can be styled as needed. * **Styleable** – States for entry and exit animations are included for easy styling, and an optional arrow element can be rendered. * **Accessible** – The trigger and popover are automatically associated semantically via ARIA. Content outside the popover is hidden from assistive technologies while it is open. The popover closes when interacting outside, or pressing the Escape key. * **Focus management** – Focus is moved into the popover on mount, and restored to the trigger element on unmount. * **Positioning** – The popover is positioned relative to the trigger element, and automatically flips and adjusts to avoid overlapping with the edge of the browser window. Note: `Popover` only provides the overlay itself. It should be combined with Dialog to create fully accessible popovers. Other overlays such as menus may also be placed in a popover. ## Anatomy# * * * A popover consists of a trigger element (e.g. button) and an overlay, which is positioned relative to the trigger. The overlay may contain a Dialog, or another element such as a Menu or ListBox when used within a component such as a Select or ComboBox. import {Button, Dialog, DialogTrigger, OverlayArrow, Popover} from 'react-aria-components'; <DialogTrigger> <Button /> <Popover> <OverlayArrow /> <Dialog /> </Popover> </DialogTrigger> import { Button, Dialog, DialogTrigger, OverlayArrow, Popover } from 'react-aria-components'; <DialogTrigger> <Button /> <Popover> <OverlayArrow /> <Dialog /> </Popover> </DialogTrigger> import { Button, Dialog, DialogTrigger, OverlayArrow, Popover } from 'react-aria-components'; <DialogTrigger> <Button /> <Popover> <OverlayArrow /> <Dialog /> </Popover> </DialogTrigger> ## Examples# * * * Account Menu A Menu with an interactive header, built with a Dialog and Popover. Notifications Popover A notifications popover styled with Tailwind CSS. ## Starter kits# * * * To help kick-start your project, we offer starter kits that include example implementations of all React Aria components with various styling solutions. All components are fully styled, including support for dark mode, high contrast mode, and all UI states. Each starter comes with a pre-configured Storybook that you can experiment with, or use as a starting point for your own component library. Vanilla CSS Download ZIP Preview Tailwind CSS Download ZIP Preview ## Reusable wrappers# * * * If you will use a Popover in multiple places in your app, you can wrap all of the pieces into a reusable component. This way, the DOM structure, styling code, and other logic are defined in a single place and reused everywhere to ensure consistency. This example wraps `Popover` and all of its children together into a single component. Since the `Dialog` is built in, this means it can't be used for components like Select, Menu, and ComboBox. Exclude the dialog if your popover will be reused in these components. import type {PopoverProps} from 'react-aria-components'; interface MyPopoverProps extends Omit<PopoverProps, 'children'> { children: React.ReactNode; } function MyPopover({ children, ...props }: MyPopoverProps) { return ( <Popover {...props}> <OverlayArrow> <svg width={12} height={12} viewBox="0 0 12 12"> <path d="M0 0 L6 6 L12 0" /> </svg> </OverlayArrow> <Dialog> {children} </Dialog> </Popover> ); } <DialogTrigger> <Button aria-label="Help">ⓘ</Button> <MyPopover> <Heading slot="title">Help</Heading> <p>For help accessing your account, please contact support.</p> </MyPopover> </DialogTrigger> import type {PopoverProps} from 'react-aria-components'; interface MyPopoverProps extends Omit<PopoverProps, 'children'> { children: React.ReactNode; } function MyPopover({ children, ...props }: MyPopoverProps) { return ( <Popover {...props}> <OverlayArrow> <svg width={12} height={12} viewBox="0 0 12 12"> <path d="M0 0 L6 6 L12 0" /> </svg> </OverlayArrow> <Dialog> {children} </Dialog> </Popover> ); } <DialogTrigger> <Button aria-label="Help">ⓘ</Button> <MyPopover> <Heading slot="title">Help</Heading> <p> For help accessing your account, please contact support. </p> </MyPopover> </DialogTrigger> import type {PopoverProps} from 'react-aria-components'; interface MyPopoverProps extends Omit< PopoverProps, 'children' > { children: React.ReactNode; } function MyPopover( { children, ...props }: MyPopoverProps ) { return ( <Popover {...props}> <OverlayArrow> <svg width={12} height={12} viewBox="0 0 12 12" > <path d="M0 0 L6 6 L12 0" /> </svg> </OverlayArrow> <Dialog> {children} </Dialog> </Popover> ); } <DialogTrigger> <Button aria-label="Help"> ⓘ </Button> <MyPopover> <Heading slot="title"> Help </Heading> <p> For help accessing your account, please contact support. </p> </MyPopover> </DialogTrigger> ⓘ ## Positioning# * * * ### Placement# The popover's placement with respect to its anchor element can be adjusted using the `placement` prop. See `Placement` for a full list of available placement combinations. Popovers will also automatically flip to the opposite direction if there isn't enough space. <div style={{ display: 'flex', gap: 8 }}> <DialogTrigger> <Button>⬅️</Button> <MyPopover placement="start"> In left-to-right, this is on the left. In right-to-left, this is on the right. </MyPopover> </DialogTrigger> <DialogTrigger> <Button>⬆️</Button> <MyPopover placement="top">This popover is above the button.</MyPopover> </DialogTrigger> <DialogTrigger> <Button>⬇️</Button> <MyPopover placement="bottom"> This popover is below the button. </MyPopover> </DialogTrigger> <DialogTrigger> <Button>➡️</Button> <MyPopover placement="end"> In left-to-right, this is on the right. In right-to-left, this is on the left. </MyPopover> </DialogTrigger> </div> <div style={{ display: 'flex', gap: 8 }}> <DialogTrigger> <Button>⬅️</Button> <MyPopover placement="start"> In left-to-right, this is on the left. In right-to-left, this is on the right. </MyPopover> </DialogTrigger> <DialogTrigger> <Button>⬆️</Button> <MyPopover placement="top"> This popover is above the button. </MyPopover> </DialogTrigger> <DialogTrigger> <Button>⬇️</Button> <MyPopover placement="bottom"> This popover is below the button. </MyPopover> </DialogTrigger> <DialogTrigger> <Button>➡️</Button> <MyPopover placement="end"> In left-to-right, this is on the right. In right-to-left, this is on the left. </MyPopover> </DialogTrigger> </div> <div style={{ display: 'flex', gap: 8 }} > <DialogTrigger> <Button>⬅️</Button> <MyPopover placement="start"> In left-to-right, this is on the left. In right-to-left, this is on the right. </MyPopover> </DialogTrigger> <DialogTrigger> <Button>⬆️</Button> <MyPopover placement="top"> This popover is above the button. </MyPopover> </DialogTrigger> <DialogTrigger> <Button>⬇️</Button> <MyPopover placement="bottom"> This popover is below the button. </MyPopover> </DialogTrigger> <DialogTrigger> <Button>➡️</Button> <MyPopover placement="end"> In left-to-right, this is on the right. In right-to-left, this is on the left. </MyPopover> </DialogTrigger> </div> ⬅️⬆️⬇️➡️ ### Offset and cross offset# The popover's offset with respect to its anchor element can be adjusted using the `offset` and `crossOffset` props. The `offset` prop controls the spacing applied along the main axis between the element and its anchor element whereas the `crossOffset` prop handles the spacing applied along the cross axis. Below is a popover offset by an additional 50px above the trigger. <DialogTrigger> <Button>Offset</Button> <MyPopover placement="top" offset={50}> Offset by an additional 50px. </MyPopover> </DialogTrigger> <DialogTrigger> <Button>Offset</Button> <MyPopover placement="top" offset={50}> Offset by an additional 50px. </MyPopover> </DialogTrigger> <DialogTrigger> <Button> Offset </Button> <MyPopover placement="top" offset={50} > Offset by an additional 50px. </MyPopover> </DialogTrigger> Offset Below is a popover cross offset by an additional 100px to the right of the trigger. <DialogTrigger> <Button>Cross offset</Button> <MyPopover placement="top" crossOffset={100}> Offset by an additional 100px. </MyPopover> </DialogTrigger> <DialogTrigger> <Button>Cross offset</Button> <MyPopover placement="top" crossOffset={100}> Offset by an additional 100px. </MyPopover> </DialogTrigger> <DialogTrigger> <Button> Cross offset </Button> <MyPopover placement="top" crossOffset={100} > Offset by an additional 100px. </MyPopover> </DialogTrigger> Cross offset ### Flipping# By default, `usePopover` attempts to flip popovers on the main axis in situations where the original placement would cause it to render out of view. This can be overridden by setting `shouldFlip={false}`. To see the difference between the two options, scroll this page so that the example below is near the bottom of the window. <div className="flex-row"> <DialogTrigger> <Button>Default</Button> <MyPopover placement="bottom"> This is a popover that will flip if it can't fully render below the button. </MyPopover> </DialogTrigger> <DialogTrigger> <Button>shouldFlip=false</Button> <MyPopover placement="bottom" shouldFlip={false}> This is a popover that won't flip if it can't fully render below the button. </MyPopover> </DialogTrigger> </div> <div className="flex-row"> <DialogTrigger> <Button>Default</Button> <MyPopover placement="bottom"> This is a popover that will flip if it can't fully render below the button. </MyPopover> </DialogTrigger> <DialogTrigger> <Button>shouldFlip=false</Button> <MyPopover placement="bottom" shouldFlip={false}> This is a popover that won't flip if it can't fully render below the button. </MyPopover> </DialogTrigger> </div> <div className="flex-row"> <DialogTrigger> <Button> Default </Button> <MyPopover placement="bottom"> This is a popover that will flip if it can't fully render below the button. </MyPopover> </DialogTrigger> <DialogTrigger> <Button> shouldFlip=false </Button> <MyPopover placement="bottom" shouldFlip={false} > This is a popover that won't flip if it can't fully render below the button. </MyPopover> </DialogTrigger> </div> DefaultshouldFlip=false ### Container padding# You can control the minimum padding required between the popover and the surrounding container via the `containerPadding` prop. This affects the positioning breakpoints that determine when it will attempt to flip. The example below will maintain at least 50px between the popover and the edge of the browser window. <DialogTrigger> <Button>Container padding</Button> <MyPopover placement="top" containerPadding={50}> This is a popover. </MyPopover> </DialogTrigger> <DialogTrigger> <Button>Container padding</Button> <MyPopover placement="top" containerPadding={50}> This is a popover. </MyPopover> </DialogTrigger> <DialogTrigger> <Button> Container padding </Button> <MyPopover placement="top" containerPadding={50} > This is a popover. </MyPopover> </DialogTrigger> Container padding ## Controlled open state# * * * The above examples have shown `Popover` used within a `<DialogTrigger>`, which handles opening the popover when a button is clicked, and positioning relative to the trigger. This is convenient, but there are cases where you want to show a popover programmatically rather than as a result of a user action, or render the `<Popover>` in a different part of the JSX tree. To do this, you can manage the popover's `isOpen` state yourself and provide it as a prop to the `<Popover>` element. The `onOpenChange` prop will be called when the user closes the popover, and should be used to update your state. In addition, the `triggerRef` prop must be set to the element that the popover should be positioned relative to. function Example() { let [isOpen, setOpen] = React.useState(false); let triggerRef = React.useRef(null); return ( <> <Button onPress={() => setOpen(true)}>Trigger</Button> <span ref={triggerRef} style={{ paddingLeft: 12 }}> Popover will be positioned relative to me </span> <MyPopover triggerRef={triggerRef} isOpen={isOpen} onOpenChange={setOpen}> <Heading slot="title">Popover</Heading> <div>I'm over here!</div> </MyPopover> </> ); } function Example() { let [isOpen, setOpen] = React.useState(false); let triggerRef = React.useRef(null); return ( <> <Button onPress={() => setOpen(true)}>Trigger</Button> <span ref={triggerRef} style={{ paddingLeft: 12 }}> Popover will be positioned relative to me </span> <MyPopover triggerRef={triggerRef} isOpen={isOpen} onOpenChange={setOpen} > <Heading slot="title">Popover</Heading> <div>I'm over here!</div> </MyPopover> </> ); } function Example() { let [isOpen, setOpen] = React.useState( false ); let triggerRef = React .useRef(null); return ( <> <Button onPress={() => setOpen(true)} > Trigger </Button> <span ref={triggerRef} style={{ paddingLeft: 12 }} > Popover will be positioned relative to me </span> <MyPopover triggerRef={triggerRef} isOpen={isOpen} onOpenChange={setOpen} > <Heading slot="title"> Popover </Heading> <div> I'm over here! </div> </MyPopover> </> ); } TriggerPopover will be positioned relative to me ## Custom trigger# * * * `DialogTrigger` works out of the box with any pressable React Aria component (e.g. Button, Link, etc.). Custom trigger elements such as third party components and other DOM elements are also supported by wrapping them with the `<Pressable>` component, or using the usePress hook. import {Pressable} from 'react-aria-components'; <DialogTrigger> <Pressable> <span role="button">Custom trigger</span> </Pressable> <MyPopover> <Heading slot="title">Dialog</Heading> <p>This popover was triggered by a custom button.</p> </MyPopover> </DialogTrigger> import {Pressable} from 'react-aria-components'; <DialogTrigger> <Pressable> <span role="button">Custom trigger</span> </Pressable> <MyPopover> <Heading slot="title">Dialog</Heading> <p>This popover was triggered by a custom button.</p> </MyPopover> </DialogTrigger> import {Pressable} from 'react-aria-components'; <DialogTrigger> <Pressable> <span role="button"> Custom trigger </span> </Pressable> <MyPopover> <Heading slot="title"> Dialog </Heading> <p> This popover was triggered by a custom button. </p> </MyPopover> </DialogTrigger> Custom trigger Note that any `<Pressable>` child must have an interactive ARIA role or use an appropriate semantic HTML element so that screen readers can announce the trigger. Trigger components must forward their `ref` and spread all props to a DOM element. const CustomTrigger = React.forwardRef((props, ref) => ( <button {...props} ref={ref} /> )); const CustomTrigger = React.forwardRef((props, ref) => ( <button {...props} ref={ref} /> )); const CustomTrigger = React.forwardRef(( props, ref ) => ( <button {...props} ref={ref} /> )); ## Props# * * * ### Popover# | Name | Type | Default | Description | | --- | --- | --- | --- | | `trigger` | `string` | — | The name of the component that triggered the popover. This is reflected on the element as the `data-trigger` attribute, and can be used to provide specific styles for the popover depending on which element triggered it. | | `triggerRef` | ` RefObject <Element | null>` | — | The ref for the element which the popover positions itself with respect to. When used within a trigger component such as DialogTrigger, MenuTrigger, Select, etc., this is set automatically. It is only required when used standalone. | | `isEntering` | `boolean` | — | Whether the popover is currently performing an entry animation. | | `isExiting` | `boolean` | — | Whether the popover is currently performing an exit animation. | | `offset` | `number` | `8` | The additional offset applied along the main axis between the element and its anchor element. | | `placement` | ` Placement ` | `'bottom'` | The placement of the element with respect to its anchor element. | | `containerPadding` | `number` | `12` | The placement padding that should be applied between the element and its surrounding container. | | `crossOffset` | `number` | `0` | The additional offset applied along the cross axis between the element and its anchor element. | | `shouldFlip` | `boolean` | `true` | Whether the element should flip its orientation (e.g. top to bottom or left to right) when there is insufficient room for it to render completely. | | `isNonModal` | `boolean` | — | Whether the popover is non-modal, i.e. elements outside the popover may be interacted with by assistive technologies. Most popovers should not use this option as it may negatively impact the screen reader experience. Only use with components such as combobox, which are designed to handle this situation carefully. | | `isKeyboardDismissDisabled` | `boolean` | `false` | Whether pressing the escape key to close the popover should be disabled. Most popovers should not use this option. When set to true, an alternative way to close the popover with a keyboard must be provided. | | `shouldCloseOnInteractOutside` | `( (element: Element )) => boolean` | — | When user interacts with the argument element outside of the popover ref, return true if onClose should be called. This gives you a chance to filter out interaction with elements that should not dismiss the popover. By default, onClose will always be called on interaction outside the popover ref. | | `boundaryElement` | `Element` | `document.body` | Element that that serves as the positioning boundary. | | `scrollRef` | ` RefObject <Element | null>` | `overlayRef` | A ref for the scrollable region within the overlay. | | `shouldUpdatePosition` | `boolean` | `true` | Whether the overlay should update its position automatically. | | `arrowBoundaryOffset` | `number` | `0` | The minimum distance the arrow's edge should be from the edge of the overlay element. | | `isOpen` | `boolean` | — | Whether the overlay is open by default (controlled). | | `defaultOpen` | `boolean` | — | Whether the overlay is open by default (uncontrolled). | | `children` | `ReactNode | ( (values: PopoverRenderProps & & { defaultChildren: ReactNode | | undefined } )) => ReactNode` | — | The children of the component. A function may be provided to alter the children based on component state. | | `className` | `string | ( (values: PopoverRenderProps & & { defaultClassName: string | | undefined } )) => string` | — | The CSS className for the element. A function may be provided to compute the class based on component state. | | `style` | `CSSProperties | ( (values: PopoverRenderProps & & { defaultStyle: CSSProperties } )) => CSSProperties | undefined` | — | The inline style for the element. A function may be provided to compute the style based on component state. | Events | Name | Type | Description | | --- | --- | --- | | `onOpenChange` | `( (isOpen: boolean )) => void` | Handler that is called when the overlay's open state changes. | Layout | Name | Type | Description | | --- | --- | --- | | `slot` | `string | null` | A slot name for the component. Slots allow the component to receive props from a parent component. An explicit `null` value indicates that the local props completely override all props received from a parent. | Sizing | Name | Type | Description | | --- | --- | --- | | `maxHeight` | `number` | The maxHeight specified for the overlay element. By default, it will take all space up to the current viewport height. | Accessibility | Name | Type | Description | | --- | --- | --- | | `aria-label` | `string` | Defines a string value that labels the current element. | | `aria-labelledby` | `string` | Identifies the element (or elements) that labels the current element. | | `aria-describedby` | `string` | Identifies the element (or elements) that describes the object. | | `aria-details` | `string` | Identifies the element (or elements) that provide a detailed, extended description for the object. | ### OverlayArrow# `OverlayArrow` accepts all HTML attributes. ## Styling# * * * React Aria components can be styled in many ways, including using CSS classes, inline styles, utility classes (e.g. Tailwind), CSS-in-JS (e.g. Styled Components), etc. By default, all components include a builtin `className` attribute which can be targeted using CSS selectors. These follow the `react-aria-ComponentName` naming convention. .react-aria-Popover { /* ... */ } .react-aria-Popover { /* ... */ } .react-aria-Popover { /* ... */ } A custom `className` can also be specified on any component. This overrides the default `className` provided by React Aria with your own. <Popover className="my-popover"> {/* ... */} </Popover> <Popover className="my-popover"> {/* ... */} </Popover> <Popover className="my-popover"> {/* ... */} </Popover> In addition, some components support multiple UI states (e.g. focused, placeholder, readonly, etc.). React Aria components expose states using data attributes, which you can target in CSS selectors. For example: .react-aria-Popover[data-placement=left] { /* ... */ } .react-aria-Popover[data-placement=left] { /* ... */ } .react-aria-Popover[data-placement=left] { /* ... */ } The `className` and `style` props also accept functions which receive states for styling. This lets you dynamically determine the classes or styles to apply, which is useful when using utility CSS libraries like Tailwind. <OverlayArrow className={({ placement }) => placement === 'left' || placement === 'right' ? 'rotate-90' : 'rotate-0'} > {/* ... */} </OverlayArrow> <OverlayArrow className={({ placement }) => placement === 'left' || placement === 'right' ? 'rotate-90' : 'rotate-0'} > {/* ... */} </OverlayArrow> <OverlayArrow className={( { placement } ) => placement === 'left' || placement === 'right' ? 'rotate-90' : 'rotate-0'} > {/* ... */} </OverlayArrow> Popovers also support entry and exit animations via states exposed as data attributes and render props. `Popover` will automatically wait for any exit animations to complete before it is removed from the DOM. See the animation guide for more details. .react-aria-Popover { transition: opacity 300ms; &[data-entering], &[data-exiting] { opacity: 0; } } .react-aria-Popover { transition: opacity 300ms; &[data-entering], &[data-exiting] { opacity: 0; } } .react-aria-Popover { transition: opacity 300ms; &[data-entering], &[data-exiting] { opacity: 0; } } The states, selectors, and render props for each component used in a `Popover` are documented below. ### Popover# A `Popover` can be targeted with the `.react-aria-Popover` CSS selector, or by overriding with a custom `className`. It supports the following states and render props: | Name | CSS Selector | Description | | --- | --- | --- | | `trigger` | `[data-trigger="..."]` | The name of the component that triggered the popover, e.g. "DialogTrigger" or "ComboBox". | | `placement` | `[data-placement="left | right | top | bottom"]` | The placement of the popover relative to the trigger. | | `isEntering` | `[data-entering]` | Whether the popover is currently entering. Use this to apply animations. | | `isExiting` | `[data-exiting]` | Whether the popover is currently exiting. Use this to apply animations. | Within a DialogTrigger, the popover will have the `data-trigger="DialogTrigger"` attribute. In addition, the `--trigger-width` CSS custom property will be set on the popover, which you can use to make the popover match the width of the trigger button. .react-aria-Popover[data-trigger=DialogTrigger] { width: var(--trigger-width); } .react-aria-Popover[data-trigger=DialogTrigger] { width: var(--trigger-width); } .react-aria-Popover[data-trigger=DialogTrigger] { width: var(--trigger-width); } ### OverlayArrow# A `OverlayArrow` can be targeted with the `.react-aria-OverlayArrow` CSS selector, or by overriding with a custom `className`. It supports the following states and render props: | Name | CSS Selector | Description | | --- | --- | --- | | `placement` | `[data-placement="left | right | top | bottom"]` | The placement of the overlay relative to the trigger. | ## Advanced customization# * * * ### Contexts# All React Aria Components export a corresponding context that can be used to send props to them from a parent element. This enables you to build your own compositional APIs similar to those found in React Aria Components itself. You can send any prop or ref via context that you could pass to the corresponding component. The local props and ref on the component are merged with the ones passed via context, with the local props taking precedence (following the rules documented in mergeProps). | Component | Context | Props | Ref | | --- | --- | --- | --- | | `Popover` | `PopoverContext` | ` PopoverProps ` | `HTMLElement` | This example shows a `HelpTrigger` component that wraps a button and a popover and shows the popover when the user presses the ? key. It uses `ButtonContext` to provide a keyboard listener and ref to a nested button, and `PopoverContext` to provide the trigger ref and open state to the nested popover. import {ButtonContext, PopoverContext} from 'react-aria-components'; interface HelpTriggerProps { children?: React.ReactNode; } function HelpTrigger({ children }: HelpTriggerProps) { let triggerRef = React.useRef(null); let [isOpen, setOpen] = React.useState(false); let onKeyDown = (e: React.KeyboardEvent) => { if (e.key === '?') { setOpen(true); } }; return ( <ButtonContext.Provider value={{ onKeyDown, ref: triggerRef }}> <PopoverContext.Provider value={{ triggerRef, isOpen, onOpenChange: setOpen }} > {children} </PopoverContext.Provider> </ButtonContext.Provider> ); } <HelpTrigger> <Button>Press ? for help</Button> <MyPopover> <Heading slot="title">Help</Heading> <div>Do you need help?</div> </MyPopover> </HelpTrigger> import { ButtonContext, PopoverContext } from 'react-aria-components'; interface HelpTriggerProps { children?: React.ReactNode; } function HelpTrigger({ children }: HelpTriggerProps) { let triggerRef = React.useRef(null); let [isOpen, setOpen] = React.useState(false); let onKeyDown = (e: React.KeyboardEvent) => { if (e.key === '?') { setOpen(true); } }; return ( <ButtonContext.Provider value={{ onKeyDown, ref: triggerRef }} > <PopoverContext.Provider value={{ triggerRef, isOpen, onOpenChange: setOpen }} > {children} </PopoverContext.Provider> </ButtonContext.Provider> ); } <HelpTrigger> <Button>Press ? for help</Button> <MyPopover> <Heading slot="title">Help</Heading> <div>Do you need help?</div> </MyPopover> </HelpTrigger> import { ButtonContext, PopoverContext } from 'react-aria-components'; interface HelpTriggerProps { children?: React.ReactNode; } function HelpTrigger( { children }: HelpTriggerProps ) { let triggerRef = React .useRef(null); let [isOpen, setOpen] = React.useState( false ); let onKeyDown = ( e: React.KeyboardEvent ) => { if (e.key === '?') { setOpen(true); } }; return ( <ButtonContext.Provider value={{ onKeyDown, ref: triggerRef }} > <PopoverContext.Provider value={{ triggerRef, isOpen, onOpenChange: setOpen }} > {children} </PopoverContext.Provider> </ButtonContext.Provider> ); } <HelpTrigger> <Button> Press ? for help </Button> <MyPopover> <Heading slot="title"> Help </Heading> <div> Do you need help? </div> </MyPopover> </HelpTrigger> Press ? for help ### Hooks# If you need to customize things further, such as accessing internal state or customizing DOM structure, you can drop down to the lower level Hook-based API. See usePopover for more details. --- ## Page: https://react-spectrum.adobe.com/react-aria/Tooltip.html # Tooltip A tooltip displays a description of an element on hover or focus. <table><tbody><tr><th>install</th><td><code>yarn add react-aria-components</code></td></tr><tr><th>version</th><td>1.10.1</td></tr><tr><th>usage</th><td><code><span>import</span> {Tooltip} <span>from</span> <span>'react-aria-components'</span></code></td></tr></tbody></table> View ARIA pattern W3C View repository GitHub View package NPM ## Example# * * * import {Button, OverlayArrow, Tooltip, TooltipTrigger} from 'react-aria-components'; <TooltipTrigger> <Button>✏️</Button> <Tooltip> <OverlayArrow> <svg width={8} height={8} viewBox="0 0 8 8"> <path d="M0 0 L4 4 L8 0" /> </svg> </OverlayArrow> Edit </Tooltip> </TooltipTrigger> import { Button, OverlayArrow, Tooltip, TooltipTrigger } from 'react-aria-components'; <TooltipTrigger> <Button>✏️</Button> <Tooltip> <OverlayArrow> <svg width={8} height={8} viewBox="0 0 8 8"> <path d="M0 0 L4 4 L8 0" /> </svg> </OverlayArrow> Edit </Tooltip> </TooltipTrigger> import { Button, OverlayArrow, Tooltip, TooltipTrigger } from 'react-aria-components'; <TooltipTrigger> <Button>✏️</Button> <Tooltip> <OverlayArrow> <svg width={8} height={8} viewBox="0 0 8 8" > <path d="M0 0 L4 4 L8 0" /> </svg> </OverlayArrow> Edit </Tooltip> </TooltipTrigger> ✏️ Show CSS @import "@react-aria/example-theme"; .react-aria-Tooltip { box-shadow: 0 8px 20px rgba(0 0 0 / 0.1); border-radius: 4px; background: var(--highlight-background); color: var(--highlight-foreground); forced-color-adjust: none; outline: none; padding: 2px 8px; max-width: 150px; /* fixes FF gap */ transform: translate3d(0, 0, 0); transition: transform 200ms, opacity 200ms; &[data-entering], &[data-exiting] { transform: var(--origin); opacity: 0; } &[data-placement=top] { margin-bottom: 8px; --origin: translateY(4px); } &[data-placement=bottom] { margin-top: 8px; --origin: translateY(-4px); & .react-aria-OverlayArrow svg { transform: rotate(180deg); } } &[data-placement=right] { margin-left: 8px; --origin: translateX(-4px); & .react-aria-OverlayArrow svg { transform: rotate(90deg); } } &[data-placement=left] { margin-right: 8px; --origin: translateX(4px); & .react-aria-OverlayArrow svg { transform: rotate(-90deg); } } & .react-aria-OverlayArrow svg { display: block; fill: var(--highlight-background); } } @import "@react-aria/example-theme"; .react-aria-Tooltip { box-shadow: 0 8px 20px rgba(0 0 0 / 0.1); border-radius: 4px; background: var(--highlight-background); color: var(--highlight-foreground); forced-color-adjust: none; outline: none; padding: 2px 8px; max-width: 150px; /* fixes FF gap */ transform: translate3d(0, 0, 0); transition: transform 200ms, opacity 200ms; &[data-entering], &[data-exiting] { transform: var(--origin); opacity: 0; } &[data-placement=top] { margin-bottom: 8px; --origin: translateY(4px); } &[data-placement=bottom] { margin-top: 8px; --origin: translateY(-4px); & .react-aria-OverlayArrow svg { transform: rotate(180deg); } } &[data-placement=right] { margin-left: 8px; --origin: translateX(-4px); & .react-aria-OverlayArrow svg { transform: rotate(90deg); } } &[data-placement=left] { margin-right: 8px; --origin: translateX(4px); & .react-aria-OverlayArrow svg { transform: rotate(-90deg); } } & .react-aria-OverlayArrow svg { display: block; fill: var(--highlight-background); } } @import "@react-aria/example-theme"; .react-aria-Tooltip { box-shadow: 0 8px 20px rgba(0 0 0 / 0.1); border-radius: 4px; background: var(--highlight-background); color: var(--highlight-foreground); forced-color-adjust: none; outline: none; padding: 2px 8px; max-width: 150px; /* fixes FF gap */ transform: translate3d(0, 0, 0); transition: transform 200ms, opacity 200ms; &[data-entering], &[data-exiting] { transform: var(--origin); opacity: 0; } &[data-placement=top] { margin-bottom: 8px; --origin: translateY(4px); } &[data-placement=bottom] { margin-top: 8px; --origin: translateY(-4px); & .react-aria-OverlayArrow svg { transform: rotate(180deg); } } &[data-placement=right] { margin-left: 8px; --origin: translateX(-4px); & .react-aria-OverlayArrow svg { transform: rotate(90deg); } } &[data-placement=left] { margin-right: 8px; --origin: translateX(4px); & .react-aria-OverlayArrow svg { transform: rotate(-90deg); } } & .react-aria-OverlayArrow svg { display: block; fill: var(--highlight-background); } } ## Features# * * * The HTML `title` attribute can be used to create a tooltip, but it cannot be styled. Custom styled tooltips can be hard to build in an accessible way. `TooltipTrigger` and `Tooltip` help build fully accessible tooltips that can be styled as needed. * **Styleable** – States for entry and exit animations are included for easy styling, and an optional arrow element can be rendered. * **Accessible** – The trigger element is automatically associated with the tooltip via `aria-describedby`. Tooltips are displayed when an element receives focus. * **Hover behavior** – Tooltips display after a global delay on hover of the first tooltip, with no delay on subsequent tooltips to avoid unintended flickering. Emulated hover events on touch devices are ignored. * **Positioning** – The tooltip is positioned relative to the trigger element, and automatically flips and adjusts to avoid overlapping with the edge of the browser window. ## Anatomy# * * * A tooltip consists of two parts: the trigger element and the tooltip itself. Users may reveal the tooltip by hovering or focusing the trigger. import {Button, OverlayArrow, Tooltip, TooltipTrigger} from 'react-aria-components'; <TooltipTrigger> <Button /> <Tooltip> <OverlayArrow /> </Tooltip> </TooltipTrigger> import { Button, OverlayArrow, Tooltip, TooltipTrigger } from 'react-aria-components'; <TooltipTrigger> <Button /> <Tooltip> <OverlayArrow /> </Tooltip> </TooltipTrigger> import { Button, OverlayArrow, Tooltip, TooltipTrigger } from 'react-aria-components'; <TooltipTrigger> <Button /> <Tooltip> <OverlayArrow /> </Tooltip> </TooltipTrigger> ### Accessibility# Tooltip triggers must be focusable and hoverable in order to ensure that all users can activate them. When displayed, TooltipTrigger automatically associates the tooltip with the trigger element so that it is described by the tooltip content. **Note**: tooltips are not shown on touch screen interactions. Ensure that your UI is usable without tooltips, or use an alternative component such as a Popover to show information in an adjacent element. ## Starter kits# * * * To help kick-start your project, we offer starter kits that include example implementations of all React Aria components with various styling solutions. All components are fully styled, including support for dark mode, high contrast mode, and all UI states. Each starter comes with a pre-configured Storybook that you can experiment with, or use as a starting point for your own component library. Vanilla CSS Download ZIP Preview Tailwind CSS Download ZIP Preview ## Reusable wrappers# * * * If you will use a Tooltip in multiple places in your app, you can wrap all of the pieces into a reusable component. This way, the DOM structure, styling code, and other logic are defined in a single place and reused everywhere to ensure consistency. This example wraps `Tooltip` and all of its children together into a single component. import type {TooltipProps} from 'react-aria-components'; interface MyTooltipProps extends Omit<TooltipProps, 'children'> { children: React.ReactNode; } function MyTooltip({ children, ...props }: MyTooltipProps) { return ( <Tooltip {...props}> <OverlayArrow> <svg width={8} height={8} viewBox="0 0 8 8"> <path d="M0 0 L4 4 L8 0" /> </svg> </OverlayArrow> {children} </Tooltip> ); } <TooltipTrigger> <Button>💾</Button> <MyTooltip>Save</MyTooltip> </TooltipTrigger> import type {TooltipProps} from 'react-aria-components'; interface MyTooltipProps extends Omit<TooltipProps, 'children'> { children: React.ReactNode; } function MyTooltip({ children, ...props }: MyTooltipProps) { return ( <Tooltip {...props}> <OverlayArrow> <svg width={8} height={8} viewBox="0 0 8 8"> <path d="M0 0 L4 4 L8 0" /> </svg> </OverlayArrow> {children} </Tooltip> ); } <TooltipTrigger> <Button>💾</Button> <MyTooltip>Save</MyTooltip> </TooltipTrigger> import type {TooltipProps} from 'react-aria-components'; interface MyTooltipProps extends Omit< TooltipProps, 'children' > { children: React.ReactNode; } function MyTooltip( { children, ...props }: MyTooltipProps ) { return ( <Tooltip {...props}> <OverlayArrow> <svg width={8} height={8} viewBox="0 0 8 8" > <path d="M0 0 L4 4 L8 0" /> </svg> </OverlayArrow> {children} </Tooltip> ); } <TooltipTrigger> <Button>💾</Button> <MyTooltip> Save </MyTooltip> </TooltipTrigger> 💾 ## Interactions# * * * ### Delay# Tooltips appear after a short delay when hovering the trigger, or instantly when using keyboard focus. This delay is called the "warmup period", and it is a global timer, shared by all tooltips. Once a tooltip is displayed, other tooltips display immediately. If the user waits for the "cooldown period" before hovering another element, the warmup timer restarts. <div style={{display: 'flex', gap: 8}}> <TooltipTrigger> <Button>Hover me</Button> <MyTooltip>I come up after a delay.</MyTooltip> </TooltipTrigger> <TooltipTrigger> <Button>Then hover me</Button> <MyTooltip>If you did it quickly, I appear immediately.</MyTooltip> </TooltipTrigger> </div> <div style={{ display: 'flex', gap: 8 }}> <TooltipTrigger> <Button>Hover me</Button> <MyTooltip>I come up after a delay.</MyTooltip> </TooltipTrigger> <TooltipTrigger> <Button>Then hover me</Button> <MyTooltip> If you did it quickly, I appear immediately. </MyTooltip> </TooltipTrigger> </div> <div style={{ display: 'flex', gap: 8 }} > <TooltipTrigger> <Button> Hover me </Button> <MyTooltip> I come up after a delay. </MyTooltip> </TooltipTrigger> <TooltipTrigger> <Button> Then hover me </Button> <MyTooltip> If you did it quickly, I appear immediately. </MyTooltip> </TooltipTrigger> </div> Hover meThen hover me The delay can be adjusted for hover using the `delay` prop. <TooltipTrigger delay={0}> <Button>💾</Button> <MyTooltip>Save</MyTooltip> </TooltipTrigger> <TooltipTrigger delay={0}> <Button>💾</Button> <MyTooltip>Save</MyTooltip> </TooltipTrigger> <TooltipTrigger delay={0} > <Button>💾</Button> <MyTooltip> Save </MyTooltip> </TooltipTrigger> 💾 ### Trigger# By default, tooltips appear both on hover and on focus. The `trigger` prop can be set to `"focus"` to display the tooltip only on focus, and not on hover. <TooltipTrigger trigger="focus"> <Button>💿</Button> <MyTooltip>Burn CD</MyTooltip> </TooltipTrigger> <TooltipTrigger trigger="focus"> <Button>💿</Button> <MyTooltip>Burn CD</MyTooltip> </TooltipTrigger> <TooltipTrigger trigger="focus"> <Button>💿</Button> <MyTooltip> Burn CD </MyTooltip> </TooltipTrigger> 💿 ## Controlled open state# * * * The open state of the tooltip can be controlled via the `defaultOpen` and `isOpen` props. function Example() { let [isOpen, setOpen] = React.useState(false); return ( <> <TooltipTrigger isOpen={isOpen} onOpenChange={setOpen}> <Button>📣</Button> <MyTooltip>Notifications</MyTooltip> </TooltipTrigger> <p>Tooltip is {isOpen ? 'showing' : 'not showing'}</p> </> ); } function Example() { let [isOpen, setOpen] = React.useState(false); return ( <> <TooltipTrigger isOpen={isOpen} onOpenChange={setOpen} > <Button>📣</Button> <MyTooltip>Notifications</MyTooltip> </TooltipTrigger> <p>Tooltip is {isOpen ? 'showing' : 'not showing'}</p> </> ); } function Example() { let [isOpen, setOpen] = React.useState( false ); return ( <> <TooltipTrigger isOpen={isOpen} onOpenChange={setOpen} > <Button> 📣 </Button> <MyTooltip> Notifications </MyTooltip> </TooltipTrigger> <p> Tooltip is{' '} {isOpen ? 'showing' : 'not showing'} </p> </> ); } 📣 Tooltip is not showing ## Positioning# * * * ### Placement# The Tooltip's placement with respect to its trigger element can be adjusted using the `placement` prop. See the props table for a full list of available placement combinations. <div style={{ display: 'flex', gap: 8 }}> <TooltipTrigger> <Button>⬅️</Button> <MyTooltip placement="start"> In left-to-right, this is on the left. In right-to-left, this is on the right. </MyTooltip> </TooltipTrigger> <TooltipTrigger> <Button>⬆️</Button> <MyTooltip placement="top">This tooltip is above the button.</MyTooltip> </TooltipTrigger> <TooltipTrigger> <Button>⬇️</Button> <MyTooltip placement="bottom"> This tooltip is below the button. </MyTooltip> </TooltipTrigger> <TooltipTrigger> <Button>➡️</Button> <MyTooltip placement="end"> In left-to-right, this is on the right. In right-to-left, this is on the left. </MyTooltip> </TooltipTrigger> </div> <div style={{ display: 'flex', gap: 8 }}> <TooltipTrigger> <Button>⬅️</Button> <MyTooltip placement="start"> In left-to-right, this is on the left. In right-to-left, this is on the right. </MyTooltip> </TooltipTrigger> <TooltipTrigger> <Button>⬆️</Button> <MyTooltip placement="top"> This tooltip is above the button. </MyTooltip> </TooltipTrigger> <TooltipTrigger> <Button>⬇️</Button> <MyTooltip placement="bottom"> This tooltip is below the button. </MyTooltip> </TooltipTrigger> <TooltipTrigger> <Button>➡️</Button> <MyTooltip placement="end"> In left-to-right, this is on the right. In right-to-left, this is on the left. </MyTooltip> </TooltipTrigger> </div> <div style={{ display: 'flex', gap: 8 }} > <TooltipTrigger> <Button>⬅️</Button> <MyTooltip placement="start"> In left-to-right, this is on the left. In right-to-left, this is on the right. </MyTooltip> </TooltipTrigger> <TooltipTrigger> <Button>⬆️</Button> <MyTooltip placement="top"> This tooltip is above the button. </MyTooltip> </TooltipTrigger> <TooltipTrigger> <Button>⬇️</Button> <MyTooltip placement="bottom"> This tooltip is below the button. </MyTooltip> </TooltipTrigger> <TooltipTrigger> <Button>➡️</Button> <MyTooltip placement="end"> In left-to-right, this is on the right. In right-to-left, this is on the left. </MyTooltip> </TooltipTrigger> </div> ⬅️⬆️⬇️➡️ ### Offset and cross offset# The Tooltip's offset with respect to its trigger can be adjusted using the `offset` and `crossOffset` props. The `offset` prop controls the spacing applied along the main axis between the element and its trigger whereas the `crossOffset` prop handles the spacing applied along the cross axis. Below is a tooltip offset by an additional 50px above the trigger. <TooltipTrigger> <Button>☝️</Button> <MyTooltip offset={50}>This will shift up.</MyTooltip> </TooltipTrigger> <TooltipTrigger> <Button>☝️</Button> <MyTooltip offset={50}>This will shift up.</MyTooltip> </TooltipTrigger> <TooltipTrigger> <Button>☝️</Button> <MyTooltip offset={50} > This will shift up. </MyTooltip> </TooltipTrigger> ☝️ Below is a tooltip cross offset by an additional 100px to the right of the trigger. <TooltipTrigger> <Button>👉</Button> <MyTooltip crossOffset={60} placement="bottom"> This will shift over to the right. </MyTooltip> </TooltipTrigger> <TooltipTrigger> <Button>👉</Button> <MyTooltip crossOffset={60} placement="bottom"> This will shift over to the right. </MyTooltip> </TooltipTrigger> <TooltipTrigger> <Button>👉</Button> <MyTooltip crossOffset={60} placement="bottom" > This will shift over to the right. </MyTooltip> </TooltipTrigger> 👉 ## Disabled# * * * The `isDisabled` prop can be provided to a TooltipTrigger to disable the tooltip, without disabling the trigger it displays on. <TooltipTrigger isDisabled> <Button>🖨</Button> <MyTooltip>Print</MyTooltip> </TooltipTrigger> <TooltipTrigger isDisabled> <Button>🖨</Button> <MyTooltip>Print</MyTooltip> </TooltipTrigger> <TooltipTrigger isDisabled > <Button>🖨</Button> <MyTooltip> Print </MyTooltip> </TooltipTrigger> 🖨 ## Custom trigger# * * * `TooltipTrigger` works out of the box with any focusable React Aria component (e.g. Button, Link, etc.). Custom trigger elements such as third party components and other DOM elements are also supported by wrapping them with the `<Focusable>` component. import {Focusable} from 'react-aria-components'; <TooltipTrigger> <Focusable> <span role="button">Custom trigger</span> </Focusable> <MyTooltip>Tooltip</MyTooltip> </TooltipTrigger> import {Focusable} from 'react-aria-components'; <TooltipTrigger> <Focusable> <span role="button">Custom trigger</span> </Focusable> <MyTooltip>Tooltip</MyTooltip> </TooltipTrigger> import {Focusable} from 'react-aria-components'; <TooltipTrigger> <Focusable> <span role="button"> Custom trigger </span> </Focusable> <MyTooltip> Tooltip </MyTooltip> </TooltipTrigger> Custom trigger Note that any `<Focusable>` child must have an ARIA role or use an appropriate semantic HTML element so that screen readers can announce the content of the tooltip. Trigger components must forward their `ref` and spread all props to a DOM element. const CustomTrigger = React.forwardRef((props, ref) => ( <button {...props} ref={ref} /> )); const CustomTrigger = React.forwardRef((props, ref) => ( <button {...props} ref={ref} /> )); const CustomTrigger = React.forwardRef(( props, ref ) => ( <button {...props} ref={ref} /> )); DialogTrigger or MenuTrigger with custom triggers via `<Pressable>` also automatically work with `TooltipTrigger`. All `<Pressable>` elements are already focusable, so there's no need to wrap them in `<Focusable>` in this case. ## Props# * * * ### TooltipTrigger# | Name | Type | Default | Description | | --- | --- | --- | --- | | `children` | `ReactNode` | — | | | `isDisabled` | `boolean` | — | Whether the tooltip should be disabled, independent from the trigger. | | `delay` | `number` | `1500` | The delay time for the tooltip to show up. See guidelines. | | `closeDelay` | `number` | `500` | The delay time for the tooltip to close. See guidelines. | | `trigger` | `'focus'` | — | By default, opens for both focus and hover. Can be made to open only for focus. | | `isOpen` | `boolean` | — | Whether the overlay is open by default (controlled). | | `defaultOpen` | `boolean` | — | Whether the overlay is open by default (uncontrolled). | Events | Name | Type | Description | | --- | --- | --- | | `onOpenChange` | `( (isOpen: boolean )) => void` | Handler that is called when the overlay's open state changes. | ### Tooltip# | Name | Type | Default | Description | | --- | --- | --- | --- | | `triggerRef` | ` RefObject <Element | null>` | — | The ref for the element which the tooltip positions itself with respect to. When used within a TooltipTrigger this is set automatically. It is only required when used standalone. | | `isEntering` | `boolean` | — | Whether the tooltip is currently performing an entry animation. | | `isExiting` | `boolean` | — | Whether the tooltip is currently performing an exit animation. | | `placement` | ` Placement ` | `'top'` | The placement of the tooltip with respect to the trigger. | | `containerPadding` | `number` | `12` | The placement padding that should be applied between the element and its surrounding container. | | `offset` | `number` | `0` | The additional offset applied along the main axis between the element and its anchor element. | | `crossOffset` | `number` | `0` | The additional offset applied along the cross axis between the element and its anchor element. | | `shouldFlip` | `boolean` | `true` | Whether the element should flip its orientation (e.g. top to bottom or left to right) when there is insufficient room for it to render completely. | | `isOpen` | `boolean` | — | Whether the element is rendered. | | `arrowBoundaryOffset` | `number` | `0` | The minimum distance the arrow's edge should be from the edge of the overlay element. | | `defaultOpen` | `boolean` | — | Whether the overlay is open by default (uncontrolled). | | `children` | `ReactNode | ( (values: TooltipRenderProps & & { defaultChildren: ReactNode | | undefined } )) => ReactNode` | — | The children of the component. A function may be provided to alter the children based on component state. | | `className` | `string | ( (values: TooltipRenderProps & & { defaultClassName: string | | undefined } )) => string` | — | The CSS className for the element. A function may be provided to compute the class based on component state. | | `style` | `CSSProperties | ( (values: TooltipRenderProps & & { defaultStyle: CSSProperties } )) => CSSProperties | undefined` | — | The inline style for the element. A function may be provided to compute the style based on component state. | Events | Name | Type | Description | | --- | --- | --- | | `onOpenChange` | `( (isOpen: boolean )) => void` | Handler that is called when the overlay's open state changes. | Accessibility | Name | Type | Description | | --- | --- | --- | | `aria-label` | `string` | Defines a string value that labels the current element. | | `aria-labelledby` | `string` | Identifies the element (or elements) that labels the current element. | | `aria-describedby` | `string` | Identifies the element (or elements) that describes the object. | | `aria-details` | `string` | Identifies the element (or elements) that provide a detailed, extended description for the object. | ### OverlayArrow# `OverlayArrow` accepts all HTML attributes. ## Styling# * * * React Aria components can be styled in many ways, including using CSS classes, inline styles, utility classes (e.g. Tailwind), CSS-in-JS (e.g. Styled Components), etc. By default, all components include a builtin `className` attribute which can be targeted using CSS selectors. These follow the `react-aria-ComponentName` naming convention. .react-aria-Tooltip { /* ... */ } .react-aria-Tooltip { /* ... */ } .react-aria-Tooltip { /* ... */ } A custom `className` can also be specified on any component. This overrides the default `className` provided by React Aria with your own. <Tooltip className="my-tooltip"> {/* ... */} </Tooltip> <Tooltip className="my-tooltip"> {/* ... */} </Tooltip> <Tooltip className="my-tooltip"> {/* ... */} </Tooltip> In addition, some components support multiple UI states (e.g. focused, placeholder, readonly, etc.). React Aria components expose states using data attributes, which you can target in CSS selectors. For example: .react-aria-Tooltip[data-placement=left] { /* ... */ } .react-aria-Tooltip[data-placement=left] { /* ... */ } .react-aria-Tooltip[data-placement=left] { /* ... */ } The `className` and `style` props also accept functions which receive states for styling. This lets you dynamically determine the classes or styles to apply, which is useful when using utility CSS libraries like Tailwind. <OverlayArrow className={({ placement }) => placement === 'left' || placement === 'right' ? 'rotate-90' : 'rotate-0'} > {/* ... */} </OverlayArrow> <OverlayArrow className={({ placement }) => placement === 'left' || placement === 'right' ? 'rotate-90' : 'rotate-0'} > {/* ... */} </OverlayArrow> <OverlayArrow className={( { placement } ) => placement === 'left' || placement === 'right' ? 'rotate-90' : 'rotate-0'} > {/* ... */} </OverlayArrow> Tooltips also support entry and exit animations via states exposed as data attributes and render props. `Tooltip` will automatically wait for any exit animations to complete before it is removed from the DOM. See the animation guide for more details. .react-aria-Tooltip { transition: opacity 300ms; &[data-entering], &[data-exiting] { opacity: 0; } } .react-aria-Tooltip { transition: opacity 300ms; &[data-entering], &[data-exiting] { opacity: 0; } } .react-aria-Tooltip { transition: opacity 300ms; &[data-entering], &[data-exiting] { opacity: 0; } } The states, selectors, and render props for each component used in a `TooltipTrigger` are documented below. ### TooltipTrigger# The `TooltipTrigger` component does not render any DOM elements (it only passes through its children) so it does not support styling. If you need a wrapper element, add one yourself inside the `<TooltipTrigger>`. <TooltipTrigger> <div className="my-tooltip-trigger"> {/* ... */} </div> </TooltipTrigger> <TooltipTrigger> <div className="my-tooltip-trigger"> {/* ... */} </div> </TooltipTrigger> <TooltipTrigger> <div className="my-tooltip-trigger"> {/* ... */} </div> </TooltipTrigger> ### Tooltip# A `Tooltip` can be targeted with the `.react-aria-Tooltip` CSS selector, or by overriding with a custom `className`. It supports the following states and render props: | Name | CSS Selector | Description | | --- | --- | --- | | `placement` | `[data-placement="left | right | top | bottom"]` | The placement of the tooltip relative to the trigger. | | `isEntering` | `[data-entering]` | Whether the tooltip is currently entering. Use this to apply animations. | | `isExiting` | `[data-exiting]` | Whether the tooltip is currently exiting. Use this to apply animations. | | `state` | `—` | State of the tooltip. | ### OverlayArrow# A `OverlayArrow` can be targeted with the `.react-aria-OverlayArrow` CSS selector, or by overriding with a custom `className`. It supports the following states and render props: | Name | CSS Selector | Description | | --- | --- | --- | | `placement` | `[data-placement="left | right | top | bottom"]` | The placement of the overlay relative to the trigger. | ## Advanced customization# * * * ### State# TooltipTrigger provides an `TooltipTriggerState` object to its children via `TooltipTriggerStateContext`. This can be used to access and manipulate the tooltip trigger's state. ### Hooks# If you need to customize things further, such as accessing internal state or customizing DOM structure, you can drop down to the lower level Hook-based API. See useTooltipTrigger for more details. --- ## Page: https://react-spectrum.adobe.com/react-aria/Autocomplete.html beta # Autocomplete An autocomplete combines a TextField or SearchField with a Menu or ListBox, allowing users to search or filter a list of suggestions. <table><tbody><tr><th>install</th><td><code>yarn add react-aria-components</code></td></tr><tr><th>version</th><td>1.10.1</td></tr><tr><th>usage</th><td><code><span>import</span> {Autocomplete} <span>from</span> <span>'react-aria-components'</span></code></td></tr></tbody></table> View ARIA pattern W3C View repository GitHub View package NPM ## Example# * * * import {Autocomplete, Button, Input, Label, Menu, MenuItem, SearchField, useFilter} from 'react-aria-components'; function Example() { let { contains } = useFilter({ sensitivity: 'base' }); return ( <div className="autocomplete"> <Autocomplete filter={contains}> <SearchField> <Label>Commands</Label> <Input placeholder="Search commands...." /> <Button>✕</Button> </SearchField> <Menu> <MenuItem>Create new file...</MenuItem> <MenuItem>Create new folder...</MenuItem> <MenuItem>Assign to...</MenuItem> <MenuItem>Assign to me</MenuItem> <MenuItem>Change status...</MenuItem> <MenuItem>Change priority...</MenuItem> <MenuItem>Add label...</MenuItem> <MenuItem>Remove label...</MenuItem> </Menu> </Autocomplete> </div> ); } import { Autocomplete, Button, Input, Label, Menu, MenuItem, SearchField, useFilter } from 'react-aria-components'; function Example() { let { contains } = useFilter({ sensitivity: 'base' }); return ( <div className="autocomplete"> <Autocomplete filter={contains}> <SearchField> <Label>Commands</Label> <Input placeholder="Search commands...." /> <Button>✕</Button> </SearchField> <Menu> <MenuItem>Create new file...</MenuItem> <MenuItem>Create new folder...</MenuItem> <MenuItem>Assign to...</MenuItem> <MenuItem>Assign to me</MenuItem> <MenuItem>Change status...</MenuItem> <MenuItem>Change priority...</MenuItem> <MenuItem>Add label...</MenuItem> <MenuItem>Remove label...</MenuItem> </Menu> </Autocomplete> </div> ); } import { Autocomplete, Button, Input, Label, Menu, MenuItem, SearchField, useFilter } from 'react-aria-components'; function Example() { let { contains } = useFilter({ sensitivity: 'base' }); return ( <div className="autocomplete"> <Autocomplete filter={contains} > <SearchField> <Label> Commands </Label> <Input placeholder="Search commands...." /> <Button> ✕ </Button> </SearchField> <Menu> <MenuItem> Create new file... </MenuItem> <MenuItem> Create new folder... </MenuItem> <MenuItem> Assign to... </MenuItem> <MenuItem> Assign to me </MenuItem> <MenuItem> Change status... </MenuItem> <MenuItem> Change priority... </MenuItem> <MenuItem> Add label... </MenuItem> <MenuItem> Remove label... </MenuItem> </Menu> </Autocomplete> </div> ); } Commands✕ Create new file... Create new folder... Assign to... Assign to me Change status... Change priority... Add label... Remove label... Show CSS @import "@react-aria/example-theme"; .autocomplete { display: flex; flex-direction: column; gap: 12px; max-width: 300px; height: 180px; border: 1px solid var(--border-color); padding: 16px; border-radius: 10px; background: var(--overlay-background); .react-aria-SearchField { width: 100%; } .react-aria-Menu { flex: 1; overflow: auto; } .react-aria-Label { margin-bottom: .5em; } } @import "@react-aria/example-theme"; .autocomplete { display: flex; flex-direction: column; gap: 12px; max-width: 300px; height: 180px; border: 1px solid var(--border-color); padding: 16px; border-radius: 10px; background: var(--overlay-background); .react-aria-SearchField { width: 100%; } .react-aria-Menu { flex: 1; overflow: auto; } .react-aria-Label { margin-bottom: .5em; } } @import "@react-aria/example-theme"; .autocomplete { display: flex; flex-direction: column; gap: 12px; max-width: 300px; height: 180px; border: 1px solid var(--border-color); padding: 16px; border-radius: 10px; background: var(--overlay-background); .react-aria-SearchField { width: 100%; } .react-aria-Menu { flex: 1; overflow: auto; } .react-aria-Label { margin-bottom: .5em; } } ## Features# * * * `Autocomplete` can be used to build UI patterns such as command palettes, searchable menus, and filterable selects. * **Flexible** – Support for multiple input types and collection types, controlled and uncontrolled state, and custom filter functions. * **Keyboard navigation** – Autocomplete can be navigated using the arrow keys, along with page up/down, home/end, etc. The list of options is filtered while typing into the input, and items can be selected with the enter key. * **Accessible** – Follows the ARIA autocomplete pattern, with support for items and sections, and slots for label and description elements within each item. * **Styleable** – Items include builtin states for styling, such as hover, press, focus, selected, and disabled. **Note**: Autocomplete supports experiences where the text input and suggestion lists are siblings. For an input combined with a separate popover suggestion list, see the ComboBox component. ## Anatomy# * * * `Autocomplete` is a controller for a child text input, such as a TextField or SearchField, and a collection component such as a Menu or ListBox. It enables the user to filter a list of items, and navigate via the arrow keys while keeping focus on the input. import {Autocomplete, SearchField, Menu} from 'react-aria-components'; <Autocomplete> <SearchField /> or <TextField /> <Menu /> or <ListBox /> </Autocomplete> import { Autocomplete, Menu, SearchField } from 'react-aria-components'; <Autocomplete> <SearchField /> or <TextField /> <Menu /> or <ListBox /> </Autocomplete> import { Autocomplete, Menu, SearchField } from 'react-aria-components'; <Autocomplete> <SearchField /> or {' '} <TextField /> <Menu /> or{' '} <ListBox /> </Autocomplete> ### Concepts# `Autocomplete` makes use of the following concepts: Collections Defining collections of items, async loading, and updating items over time. Selection Interactions and data structures to represent selection. ### Composed components# An `Autocomplete` can use the following components, which may also be used standalone or reused in other components. TextField A text field allows a user to enter a plain text value with a keyboard. SearchField A search field allows a user to enter and clear a search query. Menu A menu displays a list of actions or options that a user can choose. ListBox A listbox allows a user to select one or more options from a list. ## Examples# * * * Command Palette A command palette with actions, styled with Tailwind CSS. Searchable Select A Select component with Autocomplete filtering. ## Starter kits# * * * To help kick-start your project, we offer starter kits that include example implementations of all React Aria components with various styling solutions. All components are fully styled, including support for dark mode, high contrast mode, and all UI states. Each starter comes with a pre-configured Storybook that you can experiment with, or use as a starting point for your own component library. Vanilla CSS Download ZIP Preview Tailwind CSS Download ZIP Preview ## Reusable wrappers# * * * If you will use an Autocomplete in multiple places in your app, you can wrap all of the pieces into a reusable component. This way, the DOM structure, styling code, and other logic are defined in a single place and reused everywhere to ensure consistency. This example wraps `Autocomplete` and all of its children together into a single component which accepts a `label` prop and `children`, which are passed through to the right places. The `Item` component is also wrapped to apply class names based on the current state, as described in the styling section. import type {AutocompleteProps, Key} from 'react-aria-components'; import {Menu, MenuItem} from 'react-aria-components'; import {MySearchField} from './SearchField'; interface MyAutocompleteProps<T extends object> extends Omit<AutocompleteProps, 'children'> { label?: string; placeholder?: string; items?: Iterable<T>; children: React.ReactNode | ((item: T) => React.ReactNode); onAction?: (id: Key) => void; } function MyAutocomplete<T extends object>( { label, placeholder, items, children, onAction, ...props }: MyAutocompleteProps<T> ) { let { contains } = useFilter({ sensitivity: 'base' }); return ( <div className="my-autocomplete"> <Autocomplete filter={contains} {...props}> <MySearchField label={label} placeholder={placeholder} /> <Menu items={items} onAction={onAction}> {children} </Menu> </Autocomplete> </div> ); } <MyAutocomplete label="Commands" placeholder="Search commands..."> <MenuItem>Create new file...</MenuItem> <MenuItem>Create new folder...</MenuItem> <MenuItem>Assign to...</MenuItem> <MenuItem>Assign to me</MenuItem> <MenuItem>Change status...</MenuItem> <MenuItem>Change priority...</MenuItem> <MenuItem>Add label...</MenuItem> <MenuItem>Remove label...</MenuItem> </MyAutocomplete> import type { AutocompleteProps, Key } from 'react-aria-components'; import {Menu, MenuItem} from 'react-aria-components'; import {MySearchField} from './SearchField'; interface MyAutocompleteProps<T extends object> extends Omit<AutocompleteProps, 'children'> { label?: string; placeholder?: string; items?: Iterable<T>; children: | React.ReactNode | ((item: T) => React.ReactNode); onAction?: (id: Key) => void; } function MyAutocomplete<T extends object>( { label, placeholder, items, children, onAction, ...props }: MyAutocompleteProps<T> ) { let { contains } = useFilter({ sensitivity: 'base' }); return ( <div className="my-autocomplete"> <Autocomplete filter={contains} {...props}> <MySearchField label={label} placeholder={placeholder} /> <Menu items={items} onAction={onAction}> {children} </Menu> </Autocomplete> </div> ); } <MyAutocomplete label="Commands" placeholder="Search commands..." > <MenuItem>Create new file...</MenuItem> <MenuItem>Create new folder...</MenuItem> <MenuItem>Assign to...</MenuItem> <MenuItem>Assign to me</MenuItem> <MenuItem>Change status...</MenuItem> <MenuItem>Change priority...</MenuItem> <MenuItem>Add label...</MenuItem> <MenuItem>Remove label...</MenuItem> </MyAutocomplete> import type { AutocompleteProps, Key } from 'react-aria-components'; import { Menu, MenuItem } from 'react-aria-components'; import {MySearchField} from './SearchField'; interface MyAutocompleteProps< T extends object > extends Omit< AutocompleteProps, 'children' > { label?: string; placeholder?: string; items?: Iterable<T>; children: | React.ReactNode | (( item: T ) => React.ReactNode); onAction?: ( id: Key ) => void; } function MyAutocomplete< T extends object >({ label, placeholder, items, children, onAction, ...props }: MyAutocompleteProps< T >) { let { contains } = useFilter({ sensitivity: 'base' }); return ( <div className="my-autocomplete"> <Autocomplete filter={contains} {...props} > <MySearchField label={label} placeholder={placeholder} /> <Menu items={items} onAction={onAction} > {children} </Menu> </Autocomplete> </div> ); } <MyAutocomplete label="Commands" placeholder="Search commands..." > <MenuItem> Create new file... </MenuItem> <MenuItem> Create new folder... </MenuItem> <MenuItem> Assign to... </MenuItem> <MenuItem> Assign to me </MenuItem> <MenuItem> Change status... </MenuItem> <MenuItem> Change priority... </MenuItem> <MenuItem> Add label... </MenuItem> <MenuItem> Remove label... </MenuItem> </MyAutocomplete> Commands✕ Create new file... Create new folder... Assign to... Assign to me Change status... Change priority... Add label... Remove label... Show CSS .my-autocomplete { display: flex; flex-direction: column; gap: 12px; max-width: 300px; height: 180px; border: 1px solid var(--border-color); padding: 16px; border-radius: 10px; background: var(--overlay-background); } .react-aria-SearchField { width: 100%; } .react-aria-Label { margin-bottom: .5em; } .react-aria-Menu { &[data-empty] { align-items: center; justify-content: center; font-style: italic; } } .my-autocomplete { display: flex; flex-direction: column; gap: 12px; max-width: 300px; height: 180px; border: 1px solid var(--border-color); padding: 16px; border-radius: 10px; background: var(--overlay-background); } .react-aria-SearchField { width: 100%; } .react-aria-Label { margin-bottom: .5em; } .react-aria-Menu { &[data-empty] { align-items: center; justify-content: center; font-style: italic; } } .my-autocomplete { display: flex; flex-direction: column; gap: 12px; max-width: 300px; height: 180px; border: 1px solid var(--border-color); padding: 16px; border-radius: 10px; background: var(--overlay-background); } .react-aria-SearchField { width: 100%; } .react-aria-Label { margin-bottom: .5em; } .react-aria-Menu { &[data-empty] { align-items: center; justify-content: center; font-style: italic; } } ## Value# * * * An Autocomplete's `value` is empty by default, but an initial, uncontrolled, value can be provided using the `defaultInputValue` prop. Alternatively, a controlled value can be provided using the `inputValue` prop. Note that the input value of the Autocomplete does not affect the ComboBox's selected option. function Example() { let options = [ {id: 1, name: 'Adobe Photoshop'}, {id: 2, name: 'Adobe XD'}, {id: 3, name: 'Adobe InDesign'}, {id: 4, name: 'Adobe AfterEffects'}, {id: 5, name: 'Adobe Illustrator'}, {id: 6, name: 'Adobe Lightroom'}, {id: 7, name: 'Adobe Premiere Pro'}, {id: 8, name: 'Adobe Fresco'}, {id: 9, name: 'Adobe Dreamweaver'} ]; let [value, setValue] = React.useState('Adobe XD'); return ( <div style={{display: 'flex', gap: 16, flexWrap: 'wrap'}}> <MyAutocomplete label="Adobe products (Uncontrolled)" items={options} defaultInputValue="Adobe XD"> {item => <MenuItem>{item.name}</MenuItem>} </MyAutocomplete> <MyAutocomplete label="Adobe products (Controlled)" items={options} inputValue={value} onInputChange={setValue}> {item => <MenuItem>{item.name}</MenuItem>} </MyAutocomplete> </div> ); } function Example() { let options = [ { id: 1, name: 'Adobe Photoshop' }, { id: 2, name: 'Adobe XD' }, { id: 3, name: 'Adobe InDesign' }, { id: 4, name: 'Adobe AfterEffects' }, { id: 5, name: 'Adobe Illustrator' }, { id: 6, name: 'Adobe Lightroom' }, { id: 7, name: 'Adobe Premiere Pro' }, { id: 8, name: 'Adobe Fresco' }, { id: 9, name: 'Adobe Dreamweaver' } ]; let [value, setValue] = React.useState('Adobe XD'); return ( <div style={{ display: 'flex', gap: 16, flexWrap: 'wrap' }} > <MyAutocomplete label="Adobe products (Uncontrolled)" items={options} defaultInputValue="Adobe XD" > {(item) => <MenuItem>{item.name}</MenuItem>} </MyAutocomplete> <MyAutocomplete label="Adobe products (Controlled)" items={options} inputValue={value} onInputChange={setValue} > {(item) => <MenuItem>{item.name}</MenuItem>} </MyAutocomplete> </div> ); } function Example() { let options = [ { id: 1, name: 'Adobe Photoshop' }, { id: 2, name: 'Adobe XD' }, { id: 3, name: 'Adobe InDesign' }, { id: 4, name: 'Adobe AfterEffects' }, { id: 5, name: 'Adobe Illustrator' }, { id: 6, name: 'Adobe Lightroom' }, { id: 7, name: 'Adobe Premiere Pro' }, { id: 8, name: 'Adobe Fresco' }, { id: 9, name: 'Adobe Dreamweaver' } ]; let [value, setValue] = React.useState( 'Adobe XD' ); return ( <div style={{ display: 'flex', gap: 16, flexWrap: 'wrap' }} > <MyAutocomplete label="Adobe products (Uncontrolled)" items={options} defaultInputValue="Adobe XD" > {(item) => ( <MenuItem> {item.name} </MenuItem> )} </MyAutocomplete> <MyAutocomplete label="Adobe products (Controlled)" items={options} inputValue={value} onInputChange={setValue} > {(item) => ( <MenuItem> {item.name} </MenuItem> )} </MyAutocomplete> </div> ); } Adobe products (Uncontrolled)✕ Adobe XD Adobe products (Controlled)✕ Adobe XD ## Async loading# * * * This example uses the useAsyncList hook to handle asynchronous loading and filtering of data from a server. No `filter` prop is provided to `Autocomplete` since filtering is done on the server. import {useAsyncList} from '@react-stately/data'; function AsyncLoadingExample() { let list = useAsyncList<{ name: string }>({ async load({ signal, filterText }) { let res = await fetch( `https://swapi.py4e.com/api/people/?search=${filterText}`, { signal } ); let json = await res.json(); return { items: json.results }; } }); return ( <div className="my-autocomplete"> <Autocomplete inputValue={list.filterText} onInputChange={list.setFilterText} > <MySearchField label="Star Wars Character Search" /> <Menu items={list.items} renderEmptyState={() => 'No results found.'}> {(item) => ( <MenuItem id={item.name} href={`https://www.starwars.com/databank/${ item.name.toLowerCase().replace(/\s/g, '-') }`} target="_blank" > {item.name} </MenuItem> )} </Menu> </Autocomplete> </div> ); } import {useAsyncList} from '@react-stately/data'; function AsyncLoadingExample() { let list = useAsyncList<{ name: string }>({ async load({ signal, filterText }) { let res = await fetch( `https://swapi.py4e.com/api/people/?search=${filterText}`, { signal } ); let json = await res.json(); return { items: json.results }; } }); return ( <div className="my-autocomplete"> <Autocomplete inputValue={list.filterText} onInputChange={list.setFilterText} > <MySearchField label="Star Wars Character Search" /> <Menu items={list.items} renderEmptyState={() => 'No results found.'} > {(item) => ( <MenuItem id={item.name} href={`https://www.starwars.com/databank/${ item.name.toLowerCase().replace(/\s/g, '-') }`} target="_blank" > {item.name} </MenuItem> )} </Menu> </Autocomplete> </div> ); } import {useAsyncList} from '@react-stately/data'; function AsyncLoadingExample() { let list = useAsyncList< { name: string } >({ async load( { signal, filterText } ) { let res = await fetch( `https://swapi.py4e.com/api/people/?search=${filterText}`, { signal } ); let json = await res .json(); return { items: json.results }; } }); return ( <div className="my-autocomplete"> <Autocomplete inputValue={list .filterText} onInputChange={list .setFilterText} > <MySearchField label="Star Wars Character Search" /> <Menu items={list .items} renderEmptyState={() => 'No results found.'} > {(item) => ( <MenuItem id={item .name} href={`https://www.starwars.com/databank/${ item.name .toLowerCase() .replace( /\s/g, '-' ) }`} target="_blank" > {item.name} </MenuItem> )} </Menu> </Autocomplete> </div> ); } Star Wars Character Search✕ Luke SkywalkerC-3POR2-D2Darth VaderLeia OrganaOwen LarsBeru Whitesun larsR5-D4Biggs DarklighterObi-Wan Kenobi ## Props# * * * | Name | Type | Default | Description | | --- | --- | --- | --- | | `children` | `ReactNode` | — | The children wrapped by the autocomplete. Consists of at least an input element and a collection element to filter. | | `filter` | `( (textValue: string, , inputValue: string )) => boolean` | — | An optional filter function used to determine if a option should be included in the autocomplete list. Include this if the items you are providing to your wrapped collection aren't filtered by default. | | `disableAutoFocusFirst` | `boolean` | `false` | Whether or not to focus the first item in the collection after a filter is performed. | | `inputValue` | `string` | — | The value of the autocomplete input (controlled). | | `defaultInputValue` | `string` | — | The default value of the autocomplete input (uncontrolled). | Events | Name | Type | Description | | --- | --- | --- | | `onInputChange` | `( (value: string )) => void` | Handler that is called when the autocomplete input value changes. | Layout | Name | Type | Description | | --- | --- | --- | | `slot` | `string | null` | A slot name for the component. Slots allow the component to receive props from a parent component. An explicit `null` value indicates that the local props completely override all props received from a parent. | ## Styling# * * * Since Autocomplete doesn't render any DOM elements itself, it doesn't offer any styling options. See the styling sections for TextField, SearchField, Menu, and ListBox for more information on how to style components within the Autocomplete. ## Advanced customization# * * * ### Composition# If you need to customize one of the components within an `Autocomplete`, such as `TextField`, `SearchField`, `Menu` or `ListBox`, you can create a wrapper component. This lets you customize the props passed to the component. function MyListBox(props) { return <ListBox {...props} className="my-listbox" /> } function MyListBox(props) { return <ListBox {...props} className="my-listbox" /> } function MyListBox( props ) { return ( <ListBox {...props} className="my-listbox" /> ); } ### Contexts# All React Aria Components export a corresponding context that can be used to send props to them from a parent element. This enables you to build your own compositional APIs similar to those found in React Aria Components itself. You can send any prop or ref via context that you could pass to the corresponding component. The local props and ref on the component are merged with the ones passed via context, with the local props taking precedence (following the rules documented in mergeProps). | Component | Context | Props | Ref | | --- | --- | --- | --- | | `Autocomplete` | `AutocompleteContext` | ` AutocompleteProps ` | – | ### State# Autocomplete provides an `AutocompleteState` object to its children via `AutocompleteStateContext`. This can be used to access and manipulate the autocomplete's state. ### Hooks# If you need to customize things even further, such as accessing internal state, intercepting events, or customizing the DOM structure, you can drop down to the lower level Hook-based API. See useAutocomplete for more details. --- ## Page: https://react-spectrum.adobe.com/react-aria/ComboBox.html # ComboBox A combo box combines a text input with a listbox, allowing users to filter a list of options to items matching a query. <table><tbody><tr><th>install</th><td><code>yarn add react-aria-components</code></td></tr><tr><th>version</th><td>1.10.1</td></tr><tr><th>usage</th><td><code><span>import</span> {ComboBox} <span>from</span> <span>'react-aria-components'</span></code></td></tr></tbody></table> View ARIA pattern W3C View repository GitHub View package NPM ## Example# * * * import {Button, ComboBox, Input, Label, ListBox, ListBoxItem, Popover} from 'react-aria-components'; <ComboBox> <Label>Favorite Animal</Label> <div> <Input /> <Button>▼</Button> </div> <Popover> <ListBox> <ListBoxItem>Aardvark</ListBoxItem> <ListBoxItem>Cat</ListBoxItem> <ListBoxItem>Dog</ListBoxItem> <ListBoxItem>Kangaroo</ListBoxItem> <ListBoxItem>Panda</ListBoxItem> <ListBoxItem>Snake</ListBoxItem> </ListBox> </Popover> </ComboBox> import { Button, ComboBox, Input, Label, ListBox, ListBoxItem, Popover } from 'react-aria-components'; <ComboBox> <Label>Favorite Animal</Label> <div> <Input /> <Button>▼</Button> </div> <Popover> <ListBox> <ListBoxItem>Aardvark</ListBoxItem> <ListBoxItem>Cat</ListBoxItem> <ListBoxItem>Dog</ListBoxItem> <ListBoxItem>Kangaroo</ListBoxItem> <ListBoxItem>Panda</ListBoxItem> <ListBoxItem>Snake</ListBoxItem> </ListBox> </Popover> </ComboBox> import { Button, ComboBox, Input, Label, ListBox, ListBoxItem, Popover } from 'react-aria-components'; <ComboBox> <Label> Favorite Animal </Label> <div> <Input /> <Button>▼</Button> </div> <Popover> <ListBox> <ListBoxItem> Aardvark </ListBoxItem> <ListBoxItem> Cat </ListBoxItem> <ListBoxItem> Dog </ListBoxItem> <ListBoxItem> Kangaroo </ListBoxItem> <ListBoxItem> Panda </ListBoxItem> <ListBoxItem> Snake </ListBoxItem> </ListBox> </Popover> </ComboBox> Favorite Animal ▼ Show CSS @import "@react-aria/example-theme"; .react-aria-ComboBox { color: var(--text-color); > div:has(.react-aria-Input) { display: flex; align-items: center; } .react-aria-Input { margin: 0; font-size: 1.072rem; background: var(--field-background); color: var(--field-text-color); border: 1px solid var(--border-color); border-radius: 6px; padding: 0.286rem 2rem 0.286rem 0.571rem; vertical-align: middle; outline: none; min-width: 0; &[data-focused] { outline: 2px solid var(--focus-ring-color); outline-offset: -1px; } } .react-aria-Button { background: var(--highlight-background); color: var(--highlight-foreground); forced-color-adjust: none; border-radius: 4px; border: none; margin-left: -1.714rem; width: 1.429rem; height: 1.429rem; padding: 0; font-size: 0.857rem; cursor: default; flex-shrink: 0; &[data-pressed] { box-shadow: none; background: var(--highlight-background); } } } .react-aria-Popover[data-trigger=ComboBox] { width: var(--trigger-width); .react-aria-ListBox { display: block; width: unset; max-height: inherit; min-height: unset; border: none; .react-aria-Header { padding-left: 1.571rem; } } .react-aria-ListBoxItem { padding: 0 0.571rem 0 1.571rem; &[data-focus-visible] { outline: none; } &[data-selected] { font-weight: 600; background: unset; color: var(--text-color); &::before { content: '✓'; content: '✓' / ''; alt: ' '; position: absolute; top: 4px; left: 4px; } } &[data-focused], &[data-pressed] { background: var(--highlight-background); color: var(--highlight-foreground); } } } @import "@react-aria/example-theme"; .react-aria-ComboBox { color: var(--text-color); > div:has(.react-aria-Input) { display: flex; align-items: center; } .react-aria-Input { margin: 0; font-size: 1.072rem; background: var(--field-background); color: var(--field-text-color); border: 1px solid var(--border-color); border-radius: 6px; padding: 0.286rem 2rem 0.286rem 0.571rem; vertical-align: middle; outline: none; min-width: 0; &[data-focused] { outline: 2px solid var(--focus-ring-color); outline-offset: -1px; } } .react-aria-Button { background: var(--highlight-background); color: var(--highlight-foreground); forced-color-adjust: none; border-radius: 4px; border: none; margin-left: -1.714rem; width: 1.429rem; height: 1.429rem; padding: 0; font-size: 0.857rem; cursor: default; flex-shrink: 0; &[data-pressed] { box-shadow: none; background: var(--highlight-background); } } } .react-aria-Popover[data-trigger=ComboBox] { width: var(--trigger-width); .react-aria-ListBox { display: block; width: unset; max-height: inherit; min-height: unset; border: none; .react-aria-Header { padding-left: 1.571rem; } } .react-aria-ListBoxItem { padding: 0 0.571rem 0 1.571rem; &[data-focus-visible] { outline: none; } &[data-selected] { font-weight: 600; background: unset; color: var(--text-color); &::before { content: '✓'; content: '✓' / ''; alt: ' '; position: absolute; top: 4px; left: 4px; } } &[data-focused], &[data-pressed] { background: var(--highlight-background); color: var(--highlight-foreground); } } } @import "@react-aria/example-theme"; .react-aria-ComboBox { color: var(--text-color); > div:has(.react-aria-Input) { display: flex; align-items: center; } .react-aria-Input { margin: 0; font-size: 1.072rem; background: var(--field-background); color: var(--field-text-color); border: 1px solid var(--border-color); border-radius: 6px; padding: 0.286rem 2rem 0.286rem 0.571rem; vertical-align: middle; outline: none; min-width: 0; &[data-focused] { outline: 2px solid var(--focus-ring-color); outline-offset: -1px; } } .react-aria-Button { background: var(--highlight-background); color: var(--highlight-foreground); forced-color-adjust: none; border-radius: 4px; border: none; margin-left: -1.714rem; width: 1.429rem; height: 1.429rem; padding: 0; font-size: 0.857rem; cursor: default; flex-shrink: 0; &[data-pressed] { box-shadow: none; background: var(--highlight-background); } } } .react-aria-Popover[data-trigger=ComboBox] { width: var(--trigger-width); .react-aria-ListBox { display: block; width: unset; max-height: inherit; min-height: unset; border: none; .react-aria-Header { padding-left: 1.571rem; } } .react-aria-ListBoxItem { padding: 0 0.571rem 0 1.571rem; &[data-focus-visible] { outline: none; } &[data-selected] { font-weight: 600; background: unset; color: var(--text-color); &::before { content: '✓'; content: '✓' / ''; alt: ' '; position: absolute; top: 4px; left: 4px; } } &[data-focused], &[data-pressed] { background: var(--highlight-background); color: var(--highlight-foreground); } } } ## Features# * * * A combo box can be built using the <datalist> HTML element, but this is very limited in functionality and difficult to style. `ComboBox` helps achieve accessible combo box and autocomplete components that can be styled as needed. * **Flexible** – Support for selecting pre-defined values, custom values, controlled and uncontrolled state, custom filter functions, async loading, disabled items, validation, and multiple menu trigger options. * **Keyboard navigation** – ComboBox can be opened and navigated using the arrow keys, along with page up/down, home/end, etc. The list of options is filtered while typing into the input, and items can be selected with the enter key. * **Accessible** – Follows the ARIA combobox pattern, with support for items and sections, and slots for label and description elements within each item. Custom localized announcements are included for option focusing, filtering, and selection using an ARIA live region to ensure announcements are clear and consistent. * **Validation** – Support for native HTML constraint validation with customizable UI, custom validation functions, realtime validation, and server-side validation errors. * **Styleable** – Items include builtin states for styling, such as hover, press, focus, selected, and disabled. Read our blog post for more details about the interactions and accessibility features implemented by `ComboBox`. For more flexibility when building patterns such as command palettes, searchable menus, or filterable selects, see the Autocomplete component. ## Anatomy# * * * A combo box consists of a label, an input which displays the current value, a listbox displayed in a popover, and an optional button used to toggle the popover open state. Users can type within the input to filter the available options within the list box. The list box popover may be opened by a variety of input field interactions specified by the `menuTrigger` prop provided to `ComboBox`, or by clicking or touching the button. `ComboBox` also supports optional description and error message elements, which can be used to provide more context about the field, and any validation messages. These are linked with the input via the `aria-describedby` attribute. import {Button, ComboBox, FieldError, Header, Input, Label, ListBox, ListBoxItem, ListBoxSection, Popover, Text} from 'react-aria-components'; <ComboBox> <Label /> <Input /> <Button /> <Text slot="description" /> <FieldError /> <Popover> <ListBox> <ListBoxItem> <Text slot="label" /> <Text slot="description" /> </ListBoxItem> <ListBoxSection> <Header /> <ListBoxItem /> </ListBoxSection> </ListBox> </Popover> </ComboBox> import { Button, ComboBox, FieldError, Header, Input, Label, ListBox, ListBoxItem, ListBoxSection, Popover, Text } from 'react-aria-components'; <ComboBox> <Label /> <Input /> <Button /> <Text slot="description" /> <FieldError /> <Popover> <ListBox> <ListBoxItem> <Text slot="label" /> <Text slot="description" /> </ListBoxItem> <ListBoxSection> <Header /> <ListBoxItem /> </ListBoxSection> </ListBox> </Popover> </ComboBox> import { Button, ComboBox, FieldError, Header, Input, Label, ListBox, ListBoxItem, ListBoxSection, Popover, Text } from 'react-aria-components'; <ComboBox> <Label /> <Input /> <Button /> <Text slot="description" /> <FieldError /> <Popover> <ListBox> <ListBoxItem> <Text slot="label" /> <Text slot="description" /> </ListBoxItem> <ListBoxSection> <Header /> <ListBoxItem /> </ListBoxSection> </ListBox> </Popover> </ComboBox> If the combo box does not have a visible label, an `aria-label` or `aria-labelledby` prop must be passed instead to identify it to assistive technology. ### Concepts# `ComboBox` makes use of the following concepts: Collections Defining collections of items, async loading, and updating items over time. Selection Interactions and data structures to represent selection. Forms Validating and submitting form data, and integrating with form libraries. ### Composed components# A `ComboBox` uses the following components, which may also be used standalone or reused in other components. Label A label provides context for an input element. Input An input allows a user to enter a plain text value with a keyboard. Button A button allows a user to perform an action. Popover A popover displays content in context with a trigger element. ListBox A listbox allows a user to select one or more options from a list. ## Examples# * * * User Search ComboBox A user search ComboBox styled with Tailwind CSS. ## Starter kits# * * * To help kick-start your project, we offer starter kits that include example implementations of all React Aria components with various styling solutions. All components are fully styled, including support for dark mode, high contrast mode, and all UI states. Each starter comes with a pre-configured Storybook that you can experiment with, or use as a starting point for your own component library. Vanilla CSS Download ZIP Preview Tailwind CSS Download ZIP Preview ## Reusable wrappers# * * * If you will use a ComboBox in multiple places in your app, you can wrap all of the pieces into a reusable component. This way, the DOM structure, styling code, and other logic are defined in a single place and reused everywhere to ensure consistency. This example wraps `ComboBox` and all of its children together into a single component which accepts a `label` prop and `children`, which are passed through to the right places. It also shows how to use the `description` slot to render help text, and `FieldError` component to render validation errors. The `Item` component is also wrapped to apply class names based on the current state, as described in the styling section. import type {ComboBoxProps, ListBoxItemProps, ValidationResult} from 'react-aria-components'; import {FieldError, Text} from 'react-aria-components'; interface MyComboBoxProps<T extends object> extends Omit<ComboBoxProps<T>, 'children'> { label?: string; description?: string | null; errorMessage?: string | ((validation: ValidationResult) => string); children: React.ReactNode | ((item: T) => React.ReactNode); } function MyComboBox<T extends object>( { label, description, errorMessage, children, ...props }: MyComboBoxProps<T> ) { return ( <ComboBox {...props}> <Label>{label}</Label> <div className="my-combobox-container"> <Input /> <Button>▼</Button> </div> {description && <Text slot="description">{description}</Text>} <FieldError>{errorMessage}</FieldError> <Popover> <ListBox> {children} </ListBox> </Popover> </ComboBox> ); } function MyItem(props: ListBoxItemProps) { return ( <ListBoxItem {...props} className={({ isFocused, isSelected }) => `my-item ${isFocused ? 'focused' : ''} ${isSelected ? 'selected' : ''}`} /> ); } <MyComboBox label="Ice cream flavor"> <MyItem>Chocolate</MyItem> <MyItem>Mint</MyItem> <MyItem>Strawberry</MyItem> <MyItem>Vanilla</MyItem> </MyComboBox> import type { ComboBoxProps, ListBoxItemProps, ValidationResult } from 'react-aria-components'; import {FieldError, Text} from 'react-aria-components'; interface MyComboBoxProps<T extends object> extends Omit<ComboBoxProps<T>, 'children'> { label?: string; description?: string | null; errorMessage?: | string | ((validation: ValidationResult) => string); children: | React.ReactNode | ((item: T) => React.ReactNode); } function MyComboBox<T extends object>( { label, description, errorMessage, children, ...props }: MyComboBoxProps<T> ) { return ( <ComboBox {...props}> <Label>{label}</Label> <div className="my-combobox-container"> <Input /> <Button>▼</Button> </div> {description && ( <Text slot="description">{description}</Text> )} <FieldError>{errorMessage}</FieldError> <Popover> <ListBox> {children} </ListBox> </Popover> </ComboBox> ); } function MyItem(props: ListBoxItemProps) { return ( <ListBoxItem {...props} className={({ isFocused, isSelected }) => `my-item ${isFocused ? 'focused' : ''} ${ isSelected ? 'selected' : '' }`} /> ); } <MyComboBox label="Ice cream flavor"> <MyItem>Chocolate</MyItem> <MyItem>Mint</MyItem> <MyItem>Strawberry</MyItem> <MyItem>Vanilla</MyItem> </MyComboBox> import type { ComboBoxProps, ListBoxItemProps, ValidationResult } from 'react-aria-components'; import { FieldError, Text } from 'react-aria-components'; interface MyComboBoxProps< T extends object > extends Omit< ComboBoxProps<T>, 'children' > { label?: string; description?: | string | null; errorMessage?: | string | (( validation: ValidationResult ) => string); children: | React.ReactNode | (( item: T ) => React.ReactNode); } function MyComboBox< T extends object >({ label, description, errorMessage, children, ...props }: MyComboBoxProps<T>) { return ( <ComboBox {...props}> <Label> {label} </Label> <div className="my-combobox-container"> <Input /> <Button> ▼ </Button> </div> {description && ( <Text slot="description"> {description} </Text> )} <FieldError> {errorMessage} </FieldError> <Popover> <ListBox> {children} </ListBox> </Popover> </ComboBox> ); } function MyItem( props: ListBoxItemProps ) { return ( <ListBoxItem {...props} className={( { isFocused, isSelected } ) => `my-item ${ isFocused ? 'focused' : '' } ${ isSelected ? 'selected' : '' }`} /> ); } <MyComboBox label="Ice cream flavor"> <MyItem> Chocolate </MyItem> <MyItem>Mint</MyItem> <MyItem> Strawberry </MyItem> <MyItem> Vanilla </MyItem> </MyComboBox> Ice cream flavor ▼ Show CSS .my-item { margin: 2px; padding: 4px 8px 4px 22px; border-radius: 6px; outline: none; cursor: default; color: var(--text-color); font-size: 1.072rem; position: relative; &.selected { font-weight: 600; background: none; &::before { content: '✓'; content: '✓' / ''; alt: ' '; position: absolute; top: 4px; left: 4px; } } &.focused { background: #e70073; color: white; } } @media (forced-colors: active) { .my-item.focused { background: Highlight; color: HighlightText; } } .my-item { margin: 2px; padding: 4px 8px 4px 22px; border-radius: 6px; outline: none; cursor: default; color: var(--text-color); font-size: 1.072rem; position: relative; &.selected { font-weight: 600; background: none; &::before { content: '✓'; content: '✓' / ''; alt: ' '; position: absolute; top: 4px; left: 4px; } } &.focused { background: #e70073; color: white; } } @media (forced-colors: active) { .my-item.focused { background: Highlight; color: HighlightText; } } .my-item { margin: 2px; padding: 4px 8px 4px 22px; border-radius: 6px; outline: none; cursor: default; color: var(--text-color); font-size: 1.072rem; position: relative; &.selected { font-weight: 600; background: none; &::before { content: '✓'; content: '✓' / ''; alt: ' '; position: absolute; top: 4px; left: 4px; } } &.focused { background: #e70073; color: white; } } @media (forced-colors: active) { .my-item.focused { background: Highlight; color: HighlightText; } } ## Content# * * * ComboBox follows the Collection Components API, accepting both static and dynamic collections. The examples above show static collections, which can be used when the full list of options is known ahead of time. Dynamic collections, as shown below, can be used when the options come from an external data source such as an API call, or update over time. As seen below, an iterable list of options is passed to the ComboBox using the `defaultItems` prop. Each item accepts an `id` prop, which is passed to the `onSelectionChange` handler to identify the selected item. Alternatively, if the item objects contain an `id` property, as shown in the example below, then this is used automatically and an `id` prop is not required. import type {Key} from 'react-aria-components'; function Example() { let options = [ {id: 1, name: 'Aerospace'}, {id: 2, name: 'Mechanical'}, {id: 3, name: 'Civil'}, {id: 4, name: 'Biomedical'}, {id: 5, name: 'Nuclear'}, {id: 6, name: 'Industrial'}, {id: 7, name: 'Chemical'}, {id: 8, name: 'Agricultural'}, {id: 9, name: 'Electrical'} ]; let [majorId, setMajorId] = React.useState<Key | null>(null); return ( <> <MyComboBox defaultItems={options} onSelectionChange={setMajorId}> {(item) => <ListBoxItem>{item.name}</ListBoxItem>} </MyComboBox> <p>Selected topic id: {majorId}</p> </> ); } import type {Key} from 'react-aria-components'; function Example() { let options = [ { id: 1, name: 'Aerospace' }, { id: 2, name: 'Mechanical' }, { id: 3, name: 'Civil' }, { id: 4, name: 'Biomedical' }, { id: 5, name: 'Nuclear' }, { id: 6, name: 'Industrial' }, { id: 7, name: 'Chemical' }, { id: 8, name: 'Agricultural' }, { id: 9, name: 'Electrical' } ]; let [majorId, setMajorId] = React.useState<Key | null>( null ); return ( <> <MyComboBox defaultItems={options} onSelectionChange={setMajorId} > {(item) => <ListBoxItem>{item.name}</ListBoxItem>} </MyComboBox> <p>Selected topic id: {majorId}</p> </> ); } import type {Key} from 'react-aria-components'; function Example() { let options = [ { id: 1, name: 'Aerospace' }, { id: 2, name: 'Mechanical' }, { id: 3, name: 'Civil' }, { id: 4, name: 'Biomedical' }, { id: 5, name: 'Nuclear' }, { id: 6, name: 'Industrial' }, { id: 7, name: 'Chemical' }, { id: 8, name: 'Agricultural' }, { id: 9, name: 'Electrical' } ]; let [ majorId, setMajorId ] = React.useState< Key | null >(null); return ( <> <MyComboBox defaultItems={options} onSelectionChange={setMajorId} > {(item) => ( <ListBoxItem> {item.name} </ListBoxItem> )} </MyComboBox> <p> Selected topic id: {majorId} </p> </> ); } ▼ Selected topic id: ## Value# * * * A ComboBox's `value` is empty by default, but an initial, uncontrolled, value can be provided using the `defaultInputValue` prop. Alternatively, a controlled value can be provided using the `inputValue` prop. Note that the input value of the ComboBox does not affect the ComboBox's selected option. function Example() { let options = [ {id: 1, name: 'Adobe Photoshop'}, {id: 2, name: 'Adobe XD'}, {id: 3, name: 'Adobe InDesign'}, {id: 4, name: 'Adobe AfterEffects'}, {id: 5, name: 'Adobe Illustrator'}, {id: 6, name: 'Adobe Lightroom'}, {id: 7, name: 'Adobe Premiere Pro'}, {id: 8, name: 'Adobe Fresco'}, {id: 9, name: 'Adobe Dreamweaver'} ]; let [value, setValue] = React.useState('Adobe XD'); return ( <div style={{display: 'flex', gap: 16, flexWrap: 'wrap'}}> <MyComboBox label="Adobe product (Uncontrolled)" defaultItems={options} defaultSelectedKey={2} defaultInputValue="Adobe XD"> {item => <ListBoxItem>{item.name}</ListBoxItem>} </MyComboBox> <MyComboBox label="Pick an Adobe product (Controlled)" defaultItems={options} defaultSelectedKey={2} inputValue={value} onInputChange={setValue}> {item => <ListBoxItem>{item.name}</ListBoxItem>} </MyComboBox> </div> ); } function Example() { let options = [ { id: 1, name: 'Adobe Photoshop' }, { id: 2, name: 'Adobe XD' }, { id: 3, name: 'Adobe InDesign' }, { id: 4, name: 'Adobe AfterEffects' }, { id: 5, name: 'Adobe Illustrator' }, { id: 6, name: 'Adobe Lightroom' }, { id: 7, name: 'Adobe Premiere Pro' }, { id: 8, name: 'Adobe Fresco' }, { id: 9, name: 'Adobe Dreamweaver' } ]; let [value, setValue] = React.useState('Adobe XD'); return ( <div style={{ display: 'flex', gap: 16, flexWrap: 'wrap' }} > <MyComboBox label="Adobe product (Uncontrolled)" defaultItems={options} defaultSelectedKey={2} defaultInputValue="Adobe XD" > {(item) => <ListBoxItem>{item.name}</ListBoxItem>} </MyComboBox> <MyComboBox label="Pick an Adobe product (Controlled)" defaultItems={options} defaultSelectedKey={2} inputValue={value} onInputChange={setValue} > {(item) => <ListBoxItem>{item.name}</ListBoxItem>} </MyComboBox> </div> ); } function Example() { let options = [ { id: 1, name: 'Adobe Photoshop' }, { id: 2, name: 'Adobe XD' }, { id: 3, name: 'Adobe InDesign' }, { id: 4, name: 'Adobe AfterEffects' }, { id: 5, name: 'Adobe Illustrator' }, { id: 6, name: 'Adobe Lightroom' }, { id: 7, name: 'Adobe Premiere Pro' }, { id: 8, name: 'Adobe Fresco' }, { id: 9, name: 'Adobe Dreamweaver' } ]; let [value, setValue] = React.useState( 'Adobe XD' ); return ( <div style={{ display: 'flex', gap: 16, flexWrap: 'wrap' }} > <MyComboBox label="Adobe product (Uncontrolled)" defaultItems={options} defaultSelectedKey={2} defaultInputValue="Adobe XD" > {(item) => ( <ListBoxItem> {item.name} </ListBoxItem> )} </MyComboBox> <MyComboBox label="Pick an Adobe product (Controlled)" defaultItems={options} defaultSelectedKey={2} inputValue={value} onInputChange={setValue} > {(item) => ( <ListBoxItem> {item.name} </ListBoxItem> )} </MyComboBox> </div> ); } Adobe product (Uncontrolled) ▼ Pick an Adobe product (Controlled) ▼ ### Custom values# By default, `ComboBox` doesn't allow users to specify a value that doesn't exist in the list of options and will revert the input value to the current selected value on blur. By specifying `allowsCustomValue`, this behavior is suppressed and the user is free to enter any value within the field. <MyComboBox label="Favorite Animal" allowsCustomValue> <ListBoxItem id="red panda">Red Panda</ListBoxItem> <ListBoxItem id="cat">Cat</ListBoxItem> <ListBoxItem id="dog">Dog</ListBoxItem> <ListBoxItem id="aardvark">Aardvark</ListBoxItem> <ListBoxItem id="kangaroo">Kangaroo</ListBoxItem> <ListBoxItem id="snake">Snake</ListBoxItem> </MyComboBox> <MyComboBox label="Favorite Animal" allowsCustomValue> <ListBoxItem id="red panda">Red Panda</ListBoxItem> <ListBoxItem id="cat">Cat</ListBoxItem> <ListBoxItem id="dog">Dog</ListBoxItem> <ListBoxItem id="aardvark">Aardvark</ListBoxItem> <ListBoxItem id="kangaroo">Kangaroo</ListBoxItem> <ListBoxItem id="snake">Snake</ListBoxItem> </MyComboBox> <MyComboBox label="Favorite Animal" allowsCustomValue > <ListBoxItem id="red panda"> Red Panda </ListBoxItem> <ListBoxItem id="cat"> Cat </ListBoxItem> <ListBoxItem id="dog"> Dog </ListBoxItem> <ListBoxItem id="aardvark"> Aardvark </ListBoxItem> <ListBoxItem id="kangaroo"> Kangaroo </ListBoxItem> <ListBoxItem id="snake"> Snake </ListBoxItem> </MyComboBox> Favorite Animal ▼ ### HTML forms# ComboBox supports the `name` prop for integration with HTML forms. By default, the `id` of the selected item will be submitted to the server. If the `formValue` prop is set to `"text"` or the `allowsCustomValue` prop is true, the text in the input field will be submitted instead. <div style={{display: 'flex', gap: 16, flexWrap: 'wrap'}}> <MyComboBox label="Favorite Animal" name="favoriteAnimalId" > <ListBoxItem id="panda">Panda</ListBoxItem> <ListBoxItem id="cat">Cat</ListBoxItem> <ListBoxItem id="dog">Dog</ListBoxItem> </MyComboBox> <MyComboBox label="Ice cream flavor" name="iceCream" formValue="text" allowsCustomValue > <ListBoxItem>Chocolate</ListBoxItem> <ListBoxItem>Mint</ListBoxItem> <ListBoxItem>Strawberry</ListBoxItem> <ListBoxItem>Vanilla</ListBoxItem> </MyComboBox> </div> <div style={{display: 'flex', gap: 16, flexWrap: 'wrap'}}> <MyComboBox label="Favorite Animal" name="favoriteAnimalId" > <ListBoxItem id="panda">Panda</ListBoxItem> <ListBoxItem id="cat">Cat</ListBoxItem> <ListBoxItem id="dog">Dog</ListBoxItem> </MyComboBox> <MyComboBox label="Ice cream flavor" name="iceCream" formValue="text" allowsCustomValue > <ListBoxItem>Chocolate</ListBoxItem> <ListBoxItem>Mint</ListBoxItem> <ListBoxItem>Strawberry</ListBoxItem> <ListBoxItem>Vanilla</ListBoxItem> </MyComboBox> </div> <div style={{ display: 'flex', gap: 16, flexWrap: 'wrap' }} > <MyComboBox label="Favorite Animal" name="favoriteAnimalId" > <ListBoxItem id="panda"> Panda </ListBoxItem> <ListBoxItem id="cat"> Cat </ListBoxItem> <ListBoxItem id="dog"> Dog </ListBoxItem> </MyComboBox> <MyComboBox label="Ice cream flavor" name="iceCream" formValue="text" allowsCustomValue > <ListBoxItem> Chocolate </ListBoxItem> <ListBoxItem> Mint </ListBoxItem> <ListBoxItem> Strawberry </ListBoxItem> <ListBoxItem> Vanilla </ListBoxItem> </MyComboBox> </div> Favorite Animal ▼ Ice cream flavor ▼ ## Selection# * * * Setting a selected option can be done by using the `defaultSelectedKey` or `selectedKey` prop. The selected key corresponds to the `id` of an item. See Events for more details on selection events. function Example() { let options = [ {id: 1, name: 'Adobe Photoshop'}, {id: 2, name: 'Adobe XD'}, {id: 3, name: 'Adobe InDesign'}, {id: 4, name: 'Adobe AfterEffects'}, {id: 5, name: 'Adobe Illustrator'}, {id: 6, name: 'Adobe Lightroom'}, {id: 7, name: 'Adobe Premiere Pro'}, {id: 8, name: 'Adobe Fresco'}, {id: 9, name: 'Adobe Dreamweaver'} ]; let [productId, setProductId] = React.useState<Key>(9); return ( <div style={{display: 'flex', gap: 16, flexWrap: 'wrap'}}> <MyComboBox label="Pick an Adobe product (uncontrolled)" defaultItems={options} defaultSelectedKey={9} > {item => <ListBoxItem>{item.name}</ListBoxItem>} </MyComboBox> <MyComboBox label="Pick an Adobe product (controlled)" defaultItems={options} selectedKey={productId} onSelectionChange={selected => setProductId(selected)} > {item => <ListBoxItem>{item.name}</ListBoxItem>} </MyComboBox> </div> ); } function Example() { let options = [ { id: 1, name: 'Adobe Photoshop' }, { id: 2, name: 'Adobe XD' }, { id: 3, name: 'Adobe InDesign' }, { id: 4, name: 'Adobe AfterEffects' }, { id: 5, name: 'Adobe Illustrator' }, { id: 6, name: 'Adobe Lightroom' }, { id: 7, name: 'Adobe Premiere Pro' }, { id: 8, name: 'Adobe Fresco' }, { id: 9, name: 'Adobe Dreamweaver' } ]; let [productId, setProductId] = React.useState<Key>(9); return ( <div style={{ display: 'flex', gap: 16, flexWrap: 'wrap' }} > <MyComboBox label="Pick an Adobe product (uncontrolled)" defaultItems={options} defaultSelectedKey={9} > {(item) => <ListBoxItem>{item.name}</ListBoxItem>} </MyComboBox> <MyComboBox label="Pick an Adobe product (controlled)" defaultItems={options} selectedKey={productId} onSelectionChange={(selected) => setProductId(selected)} > {(item) => <ListBoxItem>{item.name}</ListBoxItem>} </MyComboBox> </div> ); } function Example() { let options = [ { id: 1, name: 'Adobe Photoshop' }, { id: 2, name: 'Adobe XD' }, { id: 3, name: 'Adobe InDesign' }, { id: 4, name: 'Adobe AfterEffects' }, { id: 5, name: 'Adobe Illustrator' }, { id: 6, name: 'Adobe Lightroom' }, { id: 7, name: 'Adobe Premiere Pro' }, { id: 8, name: 'Adobe Fresco' }, { id: 9, name: 'Adobe Dreamweaver' } ]; let [ productId, setProductId ] = React.useState< Key >(9); return ( <div style={{ display: 'flex', gap: 16, flexWrap: 'wrap' }} > <MyComboBox label="Pick an Adobe product (uncontrolled)" defaultItems={options} defaultSelectedKey={9} > {(item) => ( <ListBoxItem> {item.name} </ListBoxItem> )} </MyComboBox> <MyComboBox label="Pick an Adobe product (controlled)" defaultItems={options} selectedKey={productId} onSelectionChange={(selected) => setProductId( selected )} > {(item) => ( <ListBoxItem> {item.name} </ListBoxItem> )} </MyComboBox> </div> ); } Pick an Adobe product (uncontrolled) ▼ Pick an Adobe product (controlled) ▼ ## Links# * * * By default, interacting with an item in a ComboBox selects it and updates the input value. Alternatively, items may be links to another page or website. This can be achieved by passing the `href` prop to the `<ListBoxItem>` component. Interacting with link items navigates to the provided URL and does not update the selection or input value. <MyComboBox label="Tech company websites"> <ListBoxItem href="https://adobe.com/" target="_blank">Adobe</ListBoxItem> <ListBoxItem href="https://apple.com/" target="_blank">Apple</ListBoxItem> <ListBoxItem href="https://google.com/" target="_blank">Google</ListBoxItem> <ListBoxItem href="https://microsoft.com/" target="_blank"> Microsoft </ListBoxItem> </MyComboBox> <MyComboBox label="Tech company websites"> <ListBoxItem href="https://adobe.com/" target="_blank"> Adobe </ListBoxItem> <ListBoxItem href="https://apple.com/" target="_blank"> Apple </ListBoxItem> <ListBoxItem href="https://google.com/" target="_blank"> Google </ListBoxItem> <ListBoxItem href="https://microsoft.com/" target="_blank" > Microsoft </ListBoxItem> </MyComboBox> <MyComboBox label="Tech company websites"> <ListBoxItem href="https://adobe.com/" target="_blank" > Adobe </ListBoxItem> <ListBoxItem href="https://apple.com/" target="_blank" > Apple </ListBoxItem> <ListBoxItem href="https://google.com/" target="_blank" > Google </ListBoxItem> <ListBoxItem href="https://microsoft.com/" target="_blank" > Microsoft </ListBoxItem> </MyComboBox> Tech company websites ▼ ### Client side routing# The `<ListBoxItem>` component works with frameworks and client side routers like Next.js and React Router. As with other React Aria components that support links, this works via the `RouterProvider` component at the root of your app. See the client side routing guide to learn how to set this up. ## Sections# * * * ComboBox supports sections in order to group options. Sections can be used by wrapping groups of items in a `ListBoxSection` element. A `<Header>` element may also be included to label the section. ### Static items# import {ListBoxSection, Header} from 'react-aria-components'; <MyComboBox label="Preferred fruit or vegetable"> <ListBoxSection> <Header>Fruit</Header> <ListBoxItem id="Apple">Apple</ListBoxItem> <ListBoxItem id="Banana">Banana</ListBoxItem> <ListBoxItem id="Orange">Orange</ListBoxItem> <ListBoxItem id="Honeydew">Honeydew</ListBoxItem> <ListBoxItem id="Grapes">Grapes</ListBoxItem> <ListBoxItem id="Watermelon">Watermelon</ListBoxItem> <ListBoxItem id="Cantaloupe">Cantaloupe</ListBoxItem> <ListBoxItem id="Pear">Pear</ListBoxItem> </ListBoxSection> <ListBoxSection> <Header>Vegetable</Header> <ListBoxItem id="Cabbage">Cabbage</ListBoxItem> <ListBoxItem id="Broccoli">Broccoli</ListBoxItem> <ListBoxItem id="Carrots">Carrots</ListBoxItem> <ListBoxItem id="Lettuce">Lettuce</ListBoxItem> <ListBoxItem id="Spinach">Spinach</ListBoxItem> <ListBoxItem id="Bok Choy">Bok Choy</ListBoxItem> <ListBoxItem id="Cauliflower">Cauliflower</ListBoxItem> <ListBoxItem id="Potatoes">Potatoes</ListBoxItem> </ListBoxSection> </MyComboBox> import { Header, ListBoxSection } from 'react-aria-components'; <MyComboBox label="Preferred fruit or vegetable"> <ListBoxSection> <Header>Fruit</Header> <ListBoxItem id="Apple">Apple</ListBoxItem> <ListBoxItem id="Banana">Banana</ListBoxItem> <ListBoxItem id="Orange">Orange</ListBoxItem> <ListBoxItem id="Honeydew">Honeydew</ListBoxItem> <ListBoxItem id="Grapes">Grapes</ListBoxItem> <ListBoxItem id="Watermelon">Watermelon</ListBoxItem> <ListBoxItem id="Cantaloupe">Cantaloupe</ListBoxItem> <ListBoxItem id="Pear">Pear</ListBoxItem> </ListBoxSection> <ListBoxSection> <Header>Vegetable</Header> <ListBoxItem id="Cabbage">Cabbage</ListBoxItem> <ListBoxItem id="Broccoli">Broccoli</ListBoxItem> <ListBoxItem id="Carrots">Carrots</ListBoxItem> <ListBoxItem id="Lettuce">Lettuce</ListBoxItem> <ListBoxItem id="Spinach">Spinach</ListBoxItem> <ListBoxItem id="Bok Choy">Bok Choy</ListBoxItem> <ListBoxItem id="Cauliflower"> Cauliflower </ListBoxItem> <ListBoxItem id="Potatoes">Potatoes</ListBoxItem> </ListBoxSection> </MyComboBox> import { Header, ListBoxSection } from 'react-aria-components'; <MyComboBox label="Preferred fruit or vegetable"> <ListBoxSection> <Header> Fruit </Header> <ListBoxItem id="Apple"> Apple </ListBoxItem> <ListBoxItem id="Banana"> Banana </ListBoxItem> <ListBoxItem id="Orange"> Orange </ListBoxItem> <ListBoxItem id="Honeydew"> Honeydew </ListBoxItem> <ListBoxItem id="Grapes"> Grapes </ListBoxItem> <ListBoxItem id="Watermelon"> Watermelon </ListBoxItem> <ListBoxItem id="Cantaloupe"> Cantaloupe </ListBoxItem> <ListBoxItem id="Pear"> Pear </ListBoxItem> </ListBoxSection> <ListBoxSection> <Header> Vegetable </Header> <ListBoxItem id="Cabbage"> Cabbage </ListBoxItem> <ListBoxItem id="Broccoli"> Broccoli </ListBoxItem> <ListBoxItem id="Carrots"> Carrots </ListBoxItem> <ListBoxItem id="Lettuce"> Lettuce </ListBoxItem> <ListBoxItem id="Spinach"> Spinach </ListBoxItem> <ListBoxItem id="Bok Choy"> Bok Choy </ListBoxItem> <ListBoxItem id="Cauliflower"> Cauliflower </ListBoxItem> <ListBoxItem id="Potatoes"> Potatoes </ListBoxItem> </ListBoxSection> </MyComboBox> Preferred fruit or vegetable ▼ ### Dynamic items# Sections used with dynamic items are populated from a hierarchical data structure. Please note that `ListBoxSection` takes an array of data using the `items` prop only. If the section also has a header, the `Collection` component can be used to render the child items. import {Collection} from 'react-aria-components'; function Example() { let options = [ {name: 'Fruit', children: [ {name: 'Apple'}, {name: 'Banana'}, {name: 'Orange'}, {name: 'Honeydew'}, {name: 'Grapes'}, {name: 'Watermelon'}, {name: 'Cantaloupe'}, {name: 'Pear'} ]}, {name: 'Vegetable', children: [ {name: 'Cabbage'}, {name: 'Broccoli'}, {name: 'Carrots'}, {name: 'Lettuce'}, {name: 'Spinach'}, {name: 'Bok Choy'}, {name: 'Cauliflower'}, {name: 'Potatoes'} ]} ]; return ( <MyComboBox label="Preferred fruit or vegetable" defaultItems={options}> {section => ( <ListBoxSection id={section.name}> <Header>{section.name}</Header> <Collection items={section.children}> {item => <ListBoxItem id={item.name}>{item.name}</ListBoxItem>} </Collection> </ListBoxSection> )} </MyComboBox> ); } import {Collection} from 'react-aria-components'; function Example() { let options = [ { name: 'Fruit', children: [ { name: 'Apple' }, { name: 'Banana' }, { name: 'Orange' }, { name: 'Honeydew' }, { name: 'Grapes' }, { name: 'Watermelon' }, { name: 'Cantaloupe' }, { name: 'Pear' } ] }, { name: 'Vegetable', children: [ { name: 'Cabbage' }, { name: 'Broccoli' }, { name: 'Carrots' }, { name: 'Lettuce' }, { name: 'Spinach' }, { name: 'Bok Choy' }, { name: 'Cauliflower' }, { name: 'Potatoes' } ] } ]; return ( <MyComboBox label="Preferred fruit or vegetable" defaultItems={options} > {(section) => ( <ListBoxSection id={section.name}> <Header>{section.name}</Header> <Collection items={section.children}> {(item) => ( <ListBoxItem id={item.name}> {item.name} </ListBoxItem> )} </Collection> </ListBoxSection> )} </MyComboBox> ); } import {Collection} from 'react-aria-components'; function Example() { let options = [ { name: 'Fruit', children: [ { name: 'Apple' }, { name: 'Banana' }, { name: 'Orange' }, { name: 'Honeydew' }, { name: 'Grapes' }, { name: 'Watermelon' }, { name: 'Cantaloupe' }, { name: 'Pear' } ] }, { name: 'Vegetable', children: [ { name: 'Cabbage' }, { name: 'Broccoli' }, { name: 'Carrots' }, { name: 'Lettuce' }, { name: 'Spinach' }, { name: 'Bok Choy' }, { name: 'Cauliflower' }, { name: 'Potatoes' } ] } ]; return ( <MyComboBox label="Preferred fruit or vegetable" defaultItems={options} > {(section) => ( <ListBoxSection id={section .name} > <Header> {section .name} </Header> <Collection items={section .children} > {(item) => ( <ListBoxItem id={item .name} > {item .name} </ListBoxItem> )} </Collection> </ListBoxSection> )} </MyComboBox> ); } Preferred fruit or vegetable ▼ ## Text slots# * * * By default, items in a ComboBox are labeled by their text contents for accessibility. ListBoxItems also support the "label" and "description" slots to separate primary and secondary content, which improves screen reader announcements and can also be used for styling purposes. **Note**: The ARIA spec prohibits listbox items from including interactive content such as buttons, checkboxes, etc. import {Text} from 'react-aria-components'; <MyComboBox label="Select action"> <ListBoxItem textValue="Add to queue"> <Text slot="label">Add to queue</Text> <Text slot="description">Add to current watch queue.</Text> </ListBoxItem> <ListBoxItem textValue="Add review"> <Text slot="label">Add review</Text> <Text slot="description">Post a review for the episode.</Text> </ListBoxItem> <ListBoxItem textValue="Subscribe to series"> <Text slot="label">Subscribe to series</Text> <Text slot="description"> Add series to your subscription list and be notified when a new episode airs. </Text> </ListBoxItem> <ListBoxItem textValue="Report"> <Text slot="label">Report</Text> <Text slot="description">Report an issue/violation.</Text> </ListBoxItem> </MyComboBox> import {Text} from 'react-aria-components'; <MyComboBox label="Select action"> <ListBoxItem textValue="Add to queue"> <Text slot="label">Add to queue</Text> <Text slot="description"> Add to current watch queue. </Text> </ListBoxItem> <ListBoxItem textValue="Add review"> <Text slot="label">Add review</Text> <Text slot="description"> Post a review for the episode. </Text> </ListBoxItem> <ListBoxItem textValue="Subscribe to series"> <Text slot="label">Subscribe to series</Text> <Text slot="description"> Add series to your subscription list and be notified when a new episode airs. </Text> </ListBoxItem> <ListBoxItem textValue="Report"> <Text slot="label">Report</Text> <Text slot="description"> Report an issue/violation. </Text> </ListBoxItem> </MyComboBox> import {Text} from 'react-aria-components'; <MyComboBox label="Select action"> <ListBoxItem textValue="Add to queue"> <Text slot="label"> Add to queue </Text> <Text slot="description"> Add to current watch queue. </Text> </ListBoxItem> <ListBoxItem textValue="Add review"> <Text slot="label"> Add review </Text> <Text slot="description"> Post a review for the episode. </Text> </ListBoxItem> <ListBoxItem textValue="Subscribe to series"> <Text slot="label"> Subscribe to series </Text> <Text slot="description"> Add series to your subscription list and be notified when a new episode airs. </Text> </ListBoxItem> <ListBoxItem textValue="Report"> <Text slot="label"> Report </Text> <Text slot="description"> Report an issue/violation. </Text> </ListBoxItem> </MyComboBox> Select action ▼ ## Events# * * * ComboBox supports selection via mouse, keyboard, and touch. You can handle all of these via the `onSelectionChange` prop. ComboBox will pass the selected `id` to the `onSelectionChange` handler. Additionally, ComboBox accepts an `onInputChange` prop which is triggered whenever the value is edited by the user, whether through typing or option selection. The example below uses `onSelectionChange` and `onInputChange` to update the selection and input value stored in React state. function Example() { let options = [ {id: 1, name: 'Aerospace'}, {id: 2, name: 'Mechanical'}, {id: 3, name: 'Civil'}, {id: 4, name: 'Biomedical'}, {id: 5, name: 'Nuclear'}, {id: 6, name: 'Industrial'}, {id: 7, name: 'Chemical'}, {id: 8, name: 'Agricultural'}, {id: 9, name: 'Electrical'} ]; let [value, setValue] = React.useState(''); let [majorId, setMajorId] = React.useState(''); let onSelectionChange = (id) => { setMajorId(id); }; let onInputChange = (value) => { setValue(value) }; return ( <> <p>Current selected major id: {majorId}</p> <p>Current input text: {value}</p> <MyComboBox label="Pick a engineering major" defaultItems={options} selectedKey={majorId} onSelectionChange={onSelectionChange} onInputChange={onInputChange} > {item => <ListBoxItem>{item.name}</ListBoxItem>} </MyComboBox> </> ); } function Example() { let options = [ {id: 1, name: 'Aerospace'}, {id: 2, name: 'Mechanical'}, {id: 3, name: 'Civil'}, {id: 4, name: 'Biomedical'}, {id: 5, name: 'Nuclear'}, {id: 6, name: 'Industrial'}, {id: 7, name: 'Chemical'}, {id: 8, name: 'Agricultural'}, {id: 9, name: 'Electrical'} ]; let [value, setValue] = React.useState(''); let [majorId, setMajorId] = React.useState(''); let onSelectionChange = (id) => { setMajorId(id); }; let onInputChange = (value) => { setValue(value) }; return ( <> <p>Current selected major id: {majorId}</p> <p>Current input text: {value}</p> <MyComboBox label="Pick a engineering major" defaultItems={options} selectedKey={majorId} onSelectionChange={onSelectionChange} onInputChange={onInputChange} > {item => <ListBoxItem>{item.name}</ListBoxItem>} </MyComboBox> </> ); } function Example() { let options = [ { id: 1, name: 'Aerospace' }, { id: 2, name: 'Mechanical' }, { id: 3, name: 'Civil' }, { id: 4, name: 'Biomedical' }, { id: 5, name: 'Nuclear' }, { id: 6, name: 'Industrial' }, { id: 7, name: 'Chemical' }, { id: 8, name: 'Agricultural' }, { id: 9, name: 'Electrical' } ]; let [value, setValue] = React.useState(''); let [ majorId, setMajorId ] = React.useState(''); let onSelectionChange = (id) => { setMajorId(id); }; let onInputChange = ( value ) => { setValue(value); }; return ( <> <p> Current selected major id:{' '} {majorId} </p> <p> Current input text: {value} </p> <MyComboBox label="Pick a engineering major" defaultItems={options} selectedKey={majorId} onSelectionChange={onSelectionChange} onInputChange={onInputChange} > {(item) => ( <ListBoxItem> {item.name} </ListBoxItem> )} </MyComboBox> </> ); } Current selected major id: Current input text: Pick a engineering major ▼ ### Custom filtering# By default, `ComboBox` uses a "contains" function from useFilter to filter the list of options. This can be overridden using the `defaultFilter` prop, or by using the `items` prop to control the filtered list. When `items` is provided rather than `defaultItems`, `ComboBox` does no filtering of its own. The following example makes the `inputValue` controlled, and updates the filtered list that is passed to the `items` prop when the input changes value. import {useFilter} from 'react-aria'; function Example() { let options = [ { id: 1, email: 'fake@email.com' }, { id: 2, email: 'anotherfake@email.com' }, { id: 3, email: 'bob@email.com' }, { id: 4, email: 'joe@email.com' }, { id: 5, email: 'yourEmail@email.com' }, { id: 6, email: 'valid@email.com' }, { id: 7, email: 'spam@email.com' }, { id: 8, email: 'newsletter@email.com' }, { id: 9, email: 'subscribe@email.com' } ]; let { startsWith } = useFilter({ sensitivity: 'base' }); let [filterValue, setFilterValue] = React.useState(''); let filteredItems = React.useMemo( () => options.filter((item) => startsWith(item.email, filterValue)), [options, filterValue] ); return ( <MyComboBox label="To:" items={filteredItems} inputValue={filterValue} onInputChange={setFilterValue} allowsCustomValue > {(item) => <ListBoxItem>{item.email}</ListBoxItem>} </MyComboBox> ); } import {useFilter} from 'react-aria'; function Example() { let options = [ { id: 1, email: 'fake@email.com' }, { id: 2, email: 'anotherfake@email.com' }, { id: 3, email: 'bob@email.com' }, { id: 4, email: 'joe@email.com' }, { id: 5, email: 'yourEmail@email.com' }, { id: 6, email: 'valid@email.com' }, { id: 7, email: 'spam@email.com' }, { id: 8, email: 'newsletter@email.com' }, { id: 9, email: 'subscribe@email.com' } ]; let { startsWith } = useFilter({ sensitivity: 'base' }); let [filterValue, setFilterValue] = React.useState(''); let filteredItems = React.useMemo( () => options.filter((item) => startsWith(item.email, filterValue) ), [options, filterValue] ); return ( <MyComboBox label="To:" items={filteredItems} inputValue={filterValue} onInputChange={setFilterValue} allowsCustomValue > {(item) => <ListBoxItem>{item.email}</ListBoxItem>} </MyComboBox> ); } import {useFilter} from 'react-aria'; function Example() { let options = [ { id: 1, email: 'fake@email.com' }, { id: 2, email: 'anotherfake@email.com' }, { id: 3, email: 'bob@email.com' }, { id: 4, email: 'joe@email.com' }, { id: 5, email: 'yourEmail@email.com' }, { id: 6, email: 'valid@email.com' }, { id: 7, email: 'spam@email.com' }, { id: 8, email: 'newsletter@email.com' }, { id: 9, email: 'subscribe@email.com' } ]; let { startsWith } = useFilter({ sensitivity: 'base' }); let [ filterValue, setFilterValue ] = React.useState(''); let filteredItems = React.useMemo( () => options.filter(( item ) => startsWith( item.email, filterValue ) ), [ options, filterValue ] ); return ( <MyComboBox label="To:" items={filteredItems} inputValue={filterValue} onInputChange={setFilterValue} allowsCustomValue > {(item) => ( <ListBoxItem> {item.email} </ListBoxItem> )} </MyComboBox> ); } To: ▼ ### Fully controlled# When a ComboBox has multiple controlled properties (e.g.`inputValue`, `selectedKey`, `items`), it is important to note that an update to one of these properties will not automatically update the others. Each interaction done in the ComboBox will only trigger its associated event handler. For example, typing in the field will only trigger `onInputChange` whereas selecting an item from the ComboBox menu will only trigger `onSelectionChange` so it is your responsibility to update the other controlled properties accordingly. Note that you should provide an `onSelectionChange` handler for a ComboBox with controlled input value and open state. This way, you can properly control the menu's open state when the user selects an option or blurs from the field. The below example demonstrates how you would construct the same example above in a completely controlled fashion. function ControlledComboBox() { let options = [ {id: 1, name: 'Aerospace'}, {id: 2, name: 'Mechanical'}, {id: 3, name: 'Civil'}, {id: 4, name: 'Biomedical'}, {id: 5, name: 'Nuclear'}, {id: 6, name: 'Industrial'}, {id: 7, name: 'Chemical'}, {id: 8, name: 'Agricultural'}, {id: 9, name: 'Electrical'} ]; let [fieldState, setFieldState] = React.useState({ selectedKey: null, inputValue: '' }); let onSelectionChange = (id: Key) => { setFieldState({ inputValue: options.find(o => o.id === id)?.name ?? '', selectedKey: id }); }; let onInputChange = (value: string) => { setFieldState(prevState => ({ inputValue: value, selectedKey: value === '' ? null : prevState.selectedKey })); }; return ( <> <p>Current selected major id: {fieldState.selectedKey}</p> <p>Current input text: {fieldState.inputValue}</p> <MyComboBox label="Pick a engineering major" defaultItems={options} selectedKey={fieldState.selectedKey} inputValue={fieldState.inputValue} onSelectionChange={onSelectionChange} onInputChange={onInputChange}> {item => <ListBoxItem>{item.name}</ListBoxItem>} </MyComboBox> </> ); } function ControlledComboBox() { let options = [ { id: 1, name: 'Aerospace' }, { id: 2, name: 'Mechanical' }, { id: 3, name: 'Civil' }, { id: 4, name: 'Biomedical' }, { id: 5, name: 'Nuclear' }, { id: 6, name: 'Industrial' }, { id: 7, name: 'Chemical' }, { id: 8, name: 'Agricultural' }, { id: 9, name: 'Electrical' } ]; let [fieldState, setFieldState] = React.useState({ selectedKey: null, inputValue: '' }); let onSelectionChange = (id: Key) => { setFieldState({ inputValue: options.find((o) => o.id === id)?.name ?? '', selectedKey: id }); }; let onInputChange = (value: string) => { setFieldState((prevState) => ({ inputValue: value, selectedKey: value === '' ? null : prevState.selectedKey })); }; return ( <> <p> Current selected major id: {fieldState.selectedKey} </p> <p>Current input text: {fieldState.inputValue}</p> <MyComboBox label="Pick a engineering major" defaultItems={options} selectedKey={fieldState.selectedKey} inputValue={fieldState.inputValue} onSelectionChange={onSelectionChange} onInputChange={onInputChange} > {(item) => <ListBoxItem>{item.name}</ListBoxItem>} </MyComboBox> </> ); } function ControlledComboBox() { let options = [ { id: 1, name: 'Aerospace' }, { id: 2, name: 'Mechanical' }, { id: 3, name: 'Civil' }, { id: 4, name: 'Biomedical' }, { id: 5, name: 'Nuclear' }, { id: 6, name: 'Industrial' }, { id: 7, name: 'Chemical' }, { id: 8, name: 'Agricultural' }, { id: 9, name: 'Electrical' } ]; let [ fieldState, setFieldState ] = React.useState({ selectedKey: null, inputValue: '' }); let onSelectionChange = (id: Key) => { setFieldState({ inputValue: options.find( (o) => o.id === id )?.name ?? '', selectedKey: id }); }; let onInputChange = ( value: string ) => { setFieldState( (prevState) => ({ inputValue: value, selectedKey: value === '' ? null : prevState .selectedKey }) ); }; return ( <> <p> Current selected major id:{' '} {fieldState .selectedKey} </p> <p> Current input text:{' '} {fieldState .inputValue} </p> <MyComboBox label="Pick a engineering major" defaultItems={options} selectedKey={fieldState .selectedKey} inputValue={fieldState .inputValue} onSelectionChange={onSelectionChange} onInputChange={onInputChange} > {(item) => ( <ListBoxItem> {item.name} </ListBoxItem> )} </MyComboBox> </> ); } Current selected major id: Current input text: Pick a engineering major ▼ ## Menu trigger behavior# * * * `ComboBox` supports three different `menuTrigger` prop values: * `input` (default): ComboBox menu opens when the user edits the input text. * `focus`: ComboBox menu opens when the user focuses the ComboBox input. * `manual`: ComboBox menu only opens when the user presses the trigger button or uses the arrow keys. The example below has `menuTrigger` set to `focus`. <MyComboBox label="Favorite Animal" menuTrigger="focus"> <ListBoxItem id="red panda">Red Panda</ListBoxItem> <ListBoxItem id="cat">Cat</ListBoxItem> <ListBoxItem id="dog">Dog</ListBoxItem> <ListBoxItem id="aardvark">Aardvark</ListBoxItem> <ListBoxItem id="kangaroo">Kangaroo</ListBoxItem> <ListBoxItem id="snake">Snake</ListBoxItem> </MyComboBox> <MyComboBox label="Favorite Animal" menuTrigger="focus"> <ListBoxItem id="red panda">Red Panda</ListBoxItem> <ListBoxItem id="cat">Cat</ListBoxItem> <ListBoxItem id="dog">Dog</ListBoxItem> <ListBoxItem id="aardvark">Aardvark</ListBoxItem> <ListBoxItem id="kangaroo">Kangaroo</ListBoxItem> <ListBoxItem id="snake">Snake</ListBoxItem> </MyComboBox> <MyComboBox label="Favorite Animal" menuTrigger="focus" > <ListBoxItem id="red panda"> Red Panda </ListBoxItem> <ListBoxItem id="cat"> Cat </ListBoxItem> <ListBoxItem id="dog"> Dog </ListBoxItem> <ListBoxItem id="aardvark"> Aardvark </ListBoxItem> <ListBoxItem id="kangaroo"> Kangaroo </ListBoxItem> <ListBoxItem id="snake"> Snake </ListBoxItem> </MyComboBox> Favorite Animal ▼ ## Asynchronous loading# * * * This example uses the useAsyncList hook to handle asynchronous loading and filtering of data from a server. You may additionally want to display a spinner to indicate the loading state to the user, or support features like infinite scroll to load more data. See this CodeSandbox for an example of a ComboBox supporting those features. import {useAsyncList} from 'react-stately'; interface Character { name: string; } function AsyncLoadingExample() { let list = useAsyncList<Character>({ async load({ signal, filterText }) { let res = await fetch( `https://swapi.py4e.com/api/people/?search=${filterText}`, { signal } ); let json = await res.json(); return { items: json.results }; } }); return ( <MyComboBox label="Star Wars Character Lookup" items={list.items} inputValue={list.filterText} onInputChange={list.setFilterText} > {(item) => <ListBoxItem id={item.name}>{item.name}</ListBoxItem>} </MyComboBox> ); } import {useAsyncList} from 'react-stately'; interface Character { name: string; } function AsyncLoadingExample() { let list = useAsyncList<Character>({ async load({ signal, filterText }) { let res = await fetch( `https://swapi.py4e.com/api/people/?search=${filterText}`, { signal } ); let json = await res.json(); return { items: json.results }; } }); return ( <MyComboBox label="Star Wars Character Lookup" items={list.items} inputValue={list.filterText} onInputChange={list.setFilterText} > {(item) => ( <ListBoxItem id={item.name}> {item.name} </ListBoxItem> )} </MyComboBox> ); } import {useAsyncList} from 'react-stately'; interface Character { name: string; } function AsyncLoadingExample() { let list = useAsyncList< Character >({ async load( { signal, filterText } ) { let res = await fetch( `https://swapi.py4e.com/api/people/?search=${filterText}`, { signal } ); let json = await res .json(); return { items: json.results }; } }); return ( <MyComboBox label="Star Wars Character Lookup" items={list.items} inputValue={list .filterText} onInputChange={list .setFilterText} > {(item) => ( <ListBoxItem id={item.name} > {item.name} </ListBoxItem> )} </MyComboBox> ); } Star Wars Character Lookup ▼ ## Disabled# * * * A `ComboBox` can be fully disabled using the isDisabled prop. <MyComboBox label="Favorite Animal" isDisabled> <ListBoxItem id="red panda">Panda</ListBoxItem> <ListBoxItem id="cat">Cat</ListBoxItem> <ListBoxItem id="dog">Dog</ListBoxItem> </MyComboBox> <MyComboBox label="Favorite Animal" isDisabled> <ListBoxItem id="red panda">Panda</ListBoxItem> <ListBoxItem id="cat">Cat</ListBoxItem> <ListBoxItem id="dog">Dog</ListBoxItem> </MyComboBox> <MyComboBox label="Favorite Animal" isDisabled > <ListBoxItem id="red panda"> Panda </ListBoxItem> <ListBoxItem id="cat"> Cat </ListBoxItem> <ListBoxItem id="dog"> Dog </ListBoxItem> </MyComboBox> Favorite Animal ▼ Show CSS .react-aria-ComboBox { .react-aria-Input { &[data-disabled] { border-color: var(--border-color-disabled); } } .react-aria-Button { &[data-disabled] { background: var(--border-color-disabled); } } } .react-aria-ComboBox { .react-aria-Input { &[data-disabled] { border-color: var(--border-color-disabled); } } .react-aria-Button { &[data-disabled] { background: var(--border-color-disabled); } } } .react-aria-ComboBox { .react-aria-Input { &[data-disabled] { border-color: var(--border-color-disabled); } } .react-aria-Button { &[data-disabled] { background: var(--border-color-disabled); } } } ### Disabled options# You can disable specific options by providing an array of keys to `ComboBox` via the `disabledKeys` prop. This will prevent options with matching keys from being pressable and receiving keyboard focus as shown in the example below. Note that you are responsible for the styling of disabled options. <MyComboBox label="Favorite Animal" disabledKeys={['cat', 'kangaroo']}> <ListBoxItem id="red panda">Red Panda</ListBoxItem> <ListBoxItem id="cat">Cat</ListBoxItem> <ListBoxItem id="dog">Dog</ListBoxItem> <ListBoxItem id="aardvark">Aardvark</ListBoxItem> <ListBoxItem id="kangaroo">Kangaroo</ListBoxItem> <ListBoxItem id="snake">Snake</ListBoxItem> </MyComboBox> <MyComboBox label="Favorite Animal" disabledKeys={['cat', 'kangaroo']} > <ListBoxItem id="red panda">Red Panda</ListBoxItem> <ListBoxItem id="cat">Cat</ListBoxItem> <ListBoxItem id="dog">Dog</ListBoxItem> <ListBoxItem id="aardvark">Aardvark</ListBoxItem> <ListBoxItem id="kangaroo">Kangaroo</ListBoxItem> <ListBoxItem id="snake">Snake</ListBoxItem> </MyComboBox> <MyComboBox label="Favorite Animal" disabledKeys={[ 'cat', 'kangaroo' ]} > <ListBoxItem id="red panda"> Red Panda </ListBoxItem> <ListBoxItem id="cat"> Cat </ListBoxItem> <ListBoxItem id="dog"> Dog </ListBoxItem> <ListBoxItem id="aardvark"> Aardvark </ListBoxItem> <ListBoxItem id="kangaroo"> Kangaroo </ListBoxItem> <ListBoxItem id="snake"> Snake </ListBoxItem> </MyComboBox> Favorite Animal ▼ ## Validation# * * * ComboBox supports the `isRequired` prop to ensure the user enters a value, as well as custom client and server-side validation. It can also be integrated with other form libraries. See the Forms guide to learn more. To display validation errors, add a `<FieldError>` element as a child of the ComboBox. This allows you to render error messages from all of the above sources with consistent custom styles. import {Form, FieldError} from 'react-aria-components'; <Form> <ComboBox name="animal" isRequired> <Label>Favorite Animal</Label> <div> <Input /> <Button>▼</Button> </div> <FieldError /> <Popover> <ListBox> <ListBoxItem>Aardvark</ListBoxItem> <ListBoxItem>Cat</ListBoxItem> <ListBoxItem>Dog</ListBoxItem> <ListBoxItem>Kangaroo</ListBoxItem> <ListBoxItem>Panda</ListBoxItem> <ListBoxItem>Snake</ListBoxItem> </ListBox> </Popover> </ComboBox> <Button type="submit">Submit</Button> </Form> import {Form, FieldError} from 'react-aria-components'; <Form> <ComboBox name="animal" isRequired> <Label>Favorite Animal</Label> <div> <Input /> <Button>▼</Button> </div> <FieldError /> <Popover> <ListBox> <ListBoxItem>Aardvark</ListBoxItem> <ListBoxItem>Cat</ListBoxItem> <ListBoxItem>Dog</ListBoxItem> <ListBoxItem>Kangaroo</ListBoxItem> <ListBoxItem>Panda</ListBoxItem> <ListBoxItem>Snake</ListBoxItem> </ListBox> </Popover> </ComboBox> <Button type="submit">Submit</Button> </Form> import { FieldError, Form } from 'react-aria-components'; <Form> <ComboBox name="animal" isRequired > <Label> Favorite Animal </Label> <div> <Input /> <Button> ▼ </Button> </div> <FieldError /> <Popover> <ListBox> <ListBoxItem> Aardvark </ListBoxItem> <ListBoxItem> Cat </ListBoxItem> <ListBoxItem> Dog </ListBoxItem> <ListBoxItem> Kangaroo </ListBoxItem> <ListBoxItem> Panda </ListBoxItem> <ListBoxItem> Snake </ListBoxItem> </ListBox> </Popover> </ComboBox> <Button type="submit"> Submit </Button> </Form> Favorite Animal ▼ Submit Show CSS .react-aria-ComboBox { .react-aria-Input { &[data-invalid]:not([data-focused]) { border-color: var(--invalid-color); } } .react-aria-FieldError { font-size: 12px; color: var(--invalid-color); } } .react-aria-ComboBox { .react-aria-Input { &[data-invalid]:not([data-focused]) { border-color: var(--invalid-color); } } .react-aria-FieldError { font-size: 12px; color: var(--invalid-color); } } .react-aria-ComboBox { .react-aria-Input { &[data-invalid]:not([data-focused]) { border-color: var(--invalid-color); } } .react-aria-FieldError { font-size: 12px; color: var(--invalid-color); } } By default, `FieldError` displays default validation messages provided by the browser. See Customizing error messages in the Forms guide to learn how to provide your own custom errors. ### Description# The `description` slot can be used to associate additional help text with a ComboBox. <ComboBox> <Label>Favorite Animal</Label> <div> <Input /> <Button>▼</Button> </div> <Text slot="description">Please select an animal.</Text> <Popover> <ListBox> <ListBoxItem>Aardvark</ListBoxItem> <ListBoxItem>Cat</ListBoxItem> <ListBoxItem>Dog</ListBoxItem> <ListBoxItem>Kangaroo</ListBoxItem> <ListBoxItem>Panda</ListBoxItem> <ListBoxItem>Snake</ListBoxItem> </ListBox> </Popover> </ComboBox> <ComboBox> <Label>Favorite Animal</Label> <div> <Input /> <Button>▼</Button> </div> <Text slot="description">Please select an animal.</Text> <Popover> <ListBox> <ListBoxItem>Aardvark</ListBoxItem> <ListBoxItem>Cat</ListBoxItem> <ListBoxItem>Dog</ListBoxItem> <ListBoxItem>Kangaroo</ListBoxItem> <ListBoxItem>Panda</ListBoxItem> <ListBoxItem>Snake</ListBoxItem> </ListBox> </Popover> </ComboBox> <ComboBox> <Label> Favorite Animal </Label> <div> <Input /> <Button>▼</Button> </div> <Text slot="description"> Please select an animal. </Text> <Popover> <ListBox> <ListBoxItem> Aardvark </ListBoxItem> <ListBoxItem> Cat </ListBoxItem> <ListBoxItem> Dog </ListBoxItem> <ListBoxItem> Kangaroo </ListBoxItem> <ListBoxItem> Panda </ListBoxItem> <ListBoxItem> Snake </ListBoxItem> </ListBox> </Popover> </ComboBox> Favorite Animal ▼ Please select an animal. Show CSS .react-aria-ComboBox { [slot=description] { font-size: 12px; } } .react-aria-ComboBox { [slot=description] { font-size: 12px; } } .react-aria-ComboBox { [slot=description] { font-size: 12px; } } ## Props# * * * ### ComboBox# | Name | Type | Default | Description | | --- | --- | --- | --- | | `defaultFilter` | `( (textValue: string, , inputValue: string )) => boolean` | — | The filter function used to determine if a option should be included in the combo box list. | | `formValue` | `'text' | 'key'` | `'key'` | Whether the text or key of the selected item is submitted as part of an HTML form. When `allowsCustomValue` is `true`, this option does not apply and the text is always submitted. | | `allowsEmptyCollection` | `boolean` | — | Whether the combo box allows the menu to be open when the collection is empty. | | `shouldFocusWrap` | `boolean` | — | Whether keyboard navigation is circular. | | `defaultItems` | `Iterable<T>` | — | The list of ComboBox items (uncontrolled). | | `items` | `Iterable<T>` | — | The list of ComboBox items (controlled). | | `inputValue` | `string` | — | The value of the ComboBox input (controlled). | | `defaultInputValue` | `string` | — | The default value of the ComboBox input (uncontrolled). | | `allowsCustomValue` | `boolean` | — | Whether the ComboBox allows a non-item matching input value to be set. | | `menuTrigger` | ` MenuTriggerAction ` | `'input'` | The interaction required to display the ComboBox menu. | | `disabledKeys` | `Iterable<Key>` | — | The item keys that are disabled. These items cannot be selected, focused, or otherwise interacted with. | | `selectedKey` | `Key | null` | — | The currently selected key in the collection (controlled). | | `defaultSelectedKey` | `Key` | — | The initial selected key in the collection (uncontrolled). | | `isDisabled` | `boolean` | — | Whether the input is disabled. | | `isReadOnly` | `boolean` | — | Whether the input can be selected but not changed by the user. | | `isRequired` | `boolean` | — | Whether user input is required on the input before form submission. | | `isInvalid` | `boolean` | — | Whether the input value is invalid. | | `validate` | `( (value: ComboBoxValidationValue )) => ValidationError | true | null | undefined` | — | A function that returns an error message if a given value is invalid. Validation errors are displayed to the user when the form is submitted if `validationBehavior="native"`. For realtime validation, use the `isInvalid` prop instead. | | `autoFocus` | `boolean` | — | Whether the element should receive focus on render. | | `name` | `string` | — | The name of the input element, used when submitting an HTML form. See MDN. | | `validationBehavior` | `'native' | 'aria'` | `'native'` | Whether to use native HTML form validation to prevent form submission when the value is missing or invalid, or mark the field as required or invalid via ARIA. | | `children` | `ReactNode | ( (values: ComboBoxRenderProps & & { defaultChildren: ReactNode | | undefined } )) => ReactNode` | — | The children of the component. A function may be provided to alter the children based on component state. | | `className` | `string | ( (values: ComboBoxRenderProps & & { defaultClassName: string | | undefined } )) => string` | — | The CSS className for the element. A function may be provided to compute the class based on component state. | | `style` | `CSSProperties | ( (values: ComboBoxRenderProps & & { defaultStyle: CSSProperties } )) => CSSProperties | undefined` | — | The inline style for the element. A function may be provided to compute the style based on component state. | Events | Name | Type | Description | | --- | --- | --- | | `onOpenChange` | `( (isOpen: boolean, , menuTrigger?: MenuTriggerAction )) => void` | Method that is called when the open state of the menu changes. Returns the new open state and the action that caused the opening of the menu. | | `onSelectionChange` | `( (key: Key | | null )) => void` | Handler that is called when the selection changes. | | `onInputChange` | `( (value: string )) => void` | Handler that is called when the ComboBox input value changes. | | `onFocus` | `( (e: FocusEvent<HTMLInputElement> )) => void` | Handler that is called when the element receives focus. | | `onBlur` | `( (e: FocusEvent<HTMLInputElement> )) => void` | Handler that is called when the element loses focus. | | `onFocusChange` | `( (isFocused: boolean )) => void` | Handler that is called when the element's focus status changes. | | `onKeyDown` | `( (e: KeyboardEvent )) => void` | Handler that is called when a key is pressed. | | `onKeyUp` | `( (e: KeyboardEvent )) => void` | Handler that is called when a key is released. | Layout | Name | Type | Description | | --- | --- | --- | | `slot` | `string | null` | A slot name for the component. Slots allow the component to receive props from a parent component. An explicit `null` value indicates that the local props completely override all props received from a parent. | Accessibility | Name | Type | Description | | --- | --- | --- | | `id` | `string` | The element's unique identifier. See MDN. | | `aria-label` | `string` | Defines a string value that labels the current element. | | `aria-labelledby` | `string` | Identifies the element (or elements) that labels the current element. | | `aria-describedby` | `string` | Identifies the element (or elements) that describes the object. | | `aria-details` | `string` | Identifies the element (or elements) that provide a detailed, extended description for the object. | ### Label# A `<Label>` accepts all props supported by the `<label>` HTML element. ### Input# An `<Input>` accepts all props supported by the `<input>` HTML element. ### Button# A `<Button>` accepts its contents as `children`. Other props such as `onPress` and `isDisabled` will be set by the `ComboBox`. Show props | Name | Type | Default | Description | | --- | --- | --- | --- | | `form` | `string` | — | The `<form>` element to associate the button with. The value of this attribute must be the id of a `<form>` in the same document. | | `formAction` | `string` | — | The URL that processes the information submitted by the button. Overrides the action attribute of the button's form owner. | | `formEncType` | `string` | — | Indicates how to encode the form data that is submitted. | | `formMethod` | `string` | — | Indicates the HTTP method used to submit the form. | | `formNoValidate` | `boolean` | — | Indicates that the form is not to be validated when it is submitted. | | `formTarget` | `string` | — | Overrides the target attribute of the button's form owner. | | `name` | `string` | — | Submitted as a pair with the button's value as part of the form data. | | `value` | `string` | — | The value associated with the button's name when it's submitted with the form data. | | `isPending` | `boolean` | — | Whether the button is in a pending state. This disables press and hover events while retaining focusability, and announces the pending state to screen readers. | | `isDisabled` | `boolean` | — | Whether the button is disabled. | | `autoFocus` | `boolean` | — | Whether the element should receive focus on render. | | `type` | `'button' | 'submit' | 'reset'` | `'button'` | The behavior of the button when used in an HTML form. | | `children` | `ReactNode | ( (values: ButtonRenderProps & & { defaultChildren: ReactNode | | undefined } )) => ReactNode` | — | The children of the component. A function may be provided to alter the children based on component state. | | `className` | `string | ( (values: ButtonRenderProps & & { defaultClassName: string | | undefined } )) => string` | — | The CSS className for the element. A function may be provided to compute the class based on component state. | | `style` | `CSSProperties | ( (values: ButtonRenderProps & & { defaultStyle: CSSProperties } )) => CSSProperties | undefined` | — | The inline style for the element. A function may be provided to compute the style based on component state. | Events | Name | Type | Description | | --- | --- | --- | | `onPress` | `( (e: PressEvent )) => void` | Handler that is called when the press is released over the target. | | `onPressStart` | `( (e: PressEvent )) => void` | Handler that is called when a press interaction starts. | | `onPressEnd` | `( (e: PressEvent )) => void` | Handler that is called when a press interaction ends, either over the target or when the pointer leaves the target. | | `onPressChange` | `( (isPressed: boolean )) => void` | Handler that is called when the press state changes. | | `onPressUp` | `( (e: PressEvent )) => void` | Handler that is called when a press is released over the target, regardless of whether it started on the target or not. | | `onClick` | `( (e: MouseEvent<FocusableElement> )) => void` | **Not recommended – use `onPress` instead.** `onClick` is an alias for `onPress` provided for compatibility with other libraries. `onPress` provides additional event details for non-mouse interactions. | | `onFocus` | `( (e: FocusEvent<Target> )) => void` | Handler that is called when the element receives focus. | | `onBlur` | `( (e: FocusEvent<Target> )) => void` | Handler that is called when the element loses focus. | | `onFocusChange` | `( (isFocused: boolean )) => void` | Handler that is called when the element's focus status changes. | | `onKeyDown` | `( (e: KeyboardEvent )) => void` | Handler that is called when a key is pressed. | | `onKeyUp` | `( (e: KeyboardEvent )) => void` | Handler that is called when a key is released. | | `onHoverStart` | `( (e: HoverEvent )) => void` | Handler that is called when a hover interaction starts. | | `onHoverEnd` | `( (e: HoverEvent )) => void` | Handler that is called when a hover interaction ends. | | `onHoverChange` | `( (isHovering: boolean )) => void` | Handler that is called when the hover state changes. | Layout | Name | Type | Description | | --- | --- | --- | | `slot` | `string | null` | A slot name for the component. Slots allow the component to receive props from a parent component. An explicit `null` value indicates that the local props completely override all props received from a parent. | Accessibility | Name | Type | Description | | --- | --- | --- | | `id` | `string` | The element's unique identifier. See MDN. | | `excludeFromTabOrder` | `boolean` | Whether to exclude the element from the sequential tab order. If true, the element will not be focusable via the keyboard by tabbing. This should be avoided except in rare scenarios where an alternative means of accessing the element or its functionality via the keyboard is available. | | `preventFocusOnPress` | `boolean` | Whether to prevent focus from moving to the button when pressing it. Caution, this can make the button inaccessible and should only be used when alternative keyboard interaction is provided, such as ComboBox's MenuTrigger or a NumberField's increment/decrement control. | | `aria-expanded` | `boolean | 'true' | 'false'` | Indicates whether the element, or another grouping element it controls, is currently expanded or collapsed. | | `aria-haspopup` | `boolean | 'menu' | 'listbox' | 'tree' | 'grid' | 'dialog' | 'true' | 'false'` | Indicates the availability and type of interactive popup element, such as menu or dialog, that can be triggered by an element. | | `aria-controls` | `string` | Identifies the element (or elements) whose contents or presence are controlled by the current element. | | `aria-pressed` | `boolean | 'true' | 'false' | 'mixed'` | Indicates the current "pressed" state of toggle buttons. | | `aria-current` | `boolean | 'true' | 'false' | 'page' | 'step' | 'location' | 'date' | 'time'` | Indicates whether this element represents the current item within a container or set of related elements. | | `aria-label` | `string` | Defines a string value that labels the current element. | | `aria-labelledby` | `string` | Identifies the element (or elements) that labels the current element. | | `aria-describedby` | `string` | Identifies the element (or elements) that describes the object. | | `aria-details` | `string` | Identifies the element (or elements) that provide a detailed, extended description for the object. | ### Popover# A `<Popover>` is a container to hold the `<ListBox>` suggestions for a ComboBox. By default, it has a `placement` of `bottom start` within a `<ComboBox>`, but this and other positioning properties may be customized. Show props | Name | Type | Default | Description | | --- | --- | --- | --- | | `trigger` | `string` | — | The name of the component that triggered the popover. This is reflected on the element as the `data-trigger` attribute, and can be used to provide specific styles for the popover depending on which element triggered it. | | `triggerRef` | ` RefObject <Element | null>` | — | The ref for the element which the popover positions itself with respect to. When used within a trigger component such as DialogTrigger, MenuTrigger, Select, etc., this is set automatically. It is only required when used standalone. | | `isEntering` | `boolean` | — | Whether the popover is currently performing an entry animation. | | `isExiting` | `boolean` | — | Whether the popover is currently performing an exit animation. | | `offset` | `number` | `8` | The additional offset applied along the main axis between the element and its anchor element. | | `placement` | ` Placement ` | `'bottom'` | The placement of the element with respect to its anchor element. | | `containerPadding` | `number` | `12` | The placement padding that should be applied between the element and its surrounding container. | | `crossOffset` | `number` | `0` | The additional offset applied along the cross axis between the element and its anchor element. | | `shouldFlip` | `boolean` | `true` | Whether the element should flip its orientation (e.g. top to bottom or left to right) when there is insufficient room for it to render completely. | | `isNonModal` | `boolean` | — | Whether the popover is non-modal, i.e. elements outside the popover may be interacted with by assistive technologies. Most popovers should not use this option as it may negatively impact the screen reader experience. Only use with components such as combobox, which are designed to handle this situation carefully. | | `isKeyboardDismissDisabled` | `boolean` | `false` | Whether pressing the escape key to close the popover should be disabled. Most popovers should not use this option. When set to true, an alternative way to close the popover with a keyboard must be provided. | | `shouldCloseOnInteractOutside` | `( (element: Element )) => boolean` | — | When user interacts with the argument element outside of the popover ref, return true if onClose should be called. This gives you a chance to filter out interaction with elements that should not dismiss the popover. By default, onClose will always be called on interaction outside the popover ref. | | `boundaryElement` | `Element` | `document.body` | Element that that serves as the positioning boundary. | | `scrollRef` | ` RefObject <Element | null>` | `overlayRef` | A ref for the scrollable region within the overlay. | | `shouldUpdatePosition` | `boolean` | `true` | Whether the overlay should update its position automatically. | | `arrowBoundaryOffset` | `number` | `0` | The minimum distance the arrow's edge should be from the edge of the overlay element. | | `isOpen` | `boolean` | — | Whether the overlay is open by default (controlled). | | `defaultOpen` | `boolean` | — | Whether the overlay is open by default (uncontrolled). | | `children` | `ReactNode | ( (values: PopoverRenderProps & & { defaultChildren: ReactNode | | undefined } )) => ReactNode` | — | The children of the component. A function may be provided to alter the children based on component state. | | `className` | `string | ( (values: PopoverRenderProps & & { defaultClassName: string | | undefined } )) => string` | — | The CSS className for the element. A function may be provided to compute the class based on component state. | | `style` | `CSSProperties | ( (values: PopoverRenderProps & & { defaultStyle: CSSProperties } )) => CSSProperties | undefined` | — | The inline style for the element. A function may be provided to compute the style based on component state. | Events | Name | Type | Description | | --- | --- | --- | | `onOpenChange` | `( (isOpen: boolean )) => void` | Handler that is called when the overlay's open state changes. | Layout | Name | Type | Description | | --- | --- | --- | | `slot` | `string | null` | A slot name for the component. Slots allow the component to receive props from a parent component. An explicit `null` value indicates that the local props completely override all props received from a parent. | Sizing | Name | Type | Description | | --- | --- | --- | | `maxHeight` | `number` | The maxHeight specified for the overlay element. By default, it will take all space up to the current viewport height. | Accessibility | Name | Type | Description | | --- | --- | --- | | `aria-label` | `string` | Defines a string value that labels the current element. | | `aria-labelledby` | `string` | Identifies the element (or elements) that labels the current element. | | `aria-describedby` | `string` | Identifies the element (or elements) that describes the object. | | `aria-details` | `string` | Identifies the element (or elements) that provide a detailed, extended description for the object. | ### ListBox# Within a `<ComboBox>`, most `<ListBox>` props are set automatically. The `<ListBox>` defines the options to display in a ComboBox. Show props | Name | Type | Default | Description | | --- | --- | --- | --- | | `selectionBehavior` | ` SelectionBehavior ` | — | How multiple selection should behave in the collection. | | `dragAndDropHooks` | ` DragAndDropHooks ` | — | The drag and drop hooks returned by `useDragAndDrop` used to enable drag and drop behavior for the ListBox. | | `renderEmptyState` | `( (props: ListBoxRenderProps )) => ReactNode` | — | Provides content to display when there are no items in the list. | | `layout` | `'stack' | 'grid'` | `'stack'` | Whether the items are arranged in a stack or grid. | | `orientation` | ` Orientation ` | `'vertical'` | The primary orientation of the items. Usually this is the direction that the collection scrolls. | | `shouldSelectOnPressUp` | `boolean` | — | Whether selection should occur on press up instead of press down. | | `shouldFocusOnHover` | `boolean` | — | Whether options should be focused when the user hovers over them. | | `escapeKeyBehavior` | `'clearSelection' | 'none'` | `'clearSelection'` | Whether pressing the escape key should clear selection in the listbox or not. Most experiences should not modify this option as it eliminates a keyboard user's ability to easily clear selection. Only use if the escape key is being handled externally or should not trigger selection clearing contextually. | | `autoFocus` | `boolean | FocusStrategy ` | — | Whether to auto focus the listbox or an option. | | `shouldFocusWrap` | `boolean` | — | Whether focus should wrap around when the end/start is reached. | | `items` | `Iterable<T>` | — | Item objects in the collection. | | `disabledKeys` | `Iterable<Key>` | — | The item keys that are disabled. These items cannot be selected, focused, or otherwise interacted with. | | `selectionMode` | ` SelectionMode ` | — | The type of selection that is allowed in the collection. | | `disallowEmptySelection` | `boolean` | — | Whether the collection allows empty selection. | | `selectedKeys` | `'all' | Iterable<Key>` | — | The currently selected keys in the collection (controlled). | | `defaultSelectedKeys` | `'all' | Iterable<Key>` | — | The initial selected keys in the collection (uncontrolled). | | `children` | `ReactNode | ( (item: object )) => ReactNode` | — | The contents of the collection. | | `dependencies` | `ReadonlyArray<any>` | — | Values that should invalidate the item cache when using dynamic collections. | | `className` | `string | ( (values: ListBoxRenderProps & & { defaultClassName: string | | undefined } )) => string` | — | The CSS className for the element. A function may be provided to compute the class based on component state. | | `style` | `CSSProperties | ( (values: ListBoxRenderProps & & { defaultStyle: CSSProperties } )) => CSSProperties | undefined` | — | The inline style for the element. A function may be provided to compute the style based on component state. | Events | Name | Type | Description | | --- | --- | --- | | `onAction` | `( (key: Key )) => void` | Handler that is called when a user performs an action on an item. The exact user event depends on the collection's `selectionBehavior` prop and the interaction modality. | | `onSelectionChange` | `( (keys: Selection )) => void` | Handler that is called when the selection changes. | | `onFocus` | `( (e: FocusEvent<Target> )) => void` | Handler that is called when the element receives focus. | | `onBlur` | `( (e: FocusEvent<Target> )) => void` | Handler that is called when the element loses focus. | | `onFocusChange` | `( (isFocused: boolean )) => void` | Handler that is called when the element's focus status changes. | | `onScroll` | `( (e: UIEvent<Element> )) => void` | Handler that is called when a user scrolls. See MDN. | Layout | Name | Type | Description | | --- | --- | --- | | `slot` | `string | null` | A slot name for the component. Slots allow the component to receive props from a parent component. An explicit `null` value indicates that the local props completely override all props received from a parent. | Accessibility | Name | Type | Description | | --- | --- | --- | | `id` | `string` | The element's unique identifier. See MDN. | | `aria-label` | `string` | Defines a string value that labels the current element. | | `aria-labelledby` | `string` | Identifies the element (or elements) that labels the current element. | | `aria-describedby` | `string` | Identifies the element (or elements) that describes the object. | | `aria-details` | `string` | Identifies the element (or elements) that provide a detailed, extended description for the object. | ### ListBoxSection# A `<ListBoxSection>` defines the child items for a section within a `<ListBox>`. It may also contain an optional `<Header>` element. If there is no header, then an `aria-label` must be provided to identify the section to assistive technologies. Show props | Name | Type | Description | | --- | --- | --- | | `id` | `Key` | The unique id of the section. | | `value` | `object` | The object value that this section represents. When using dynamic collections, this is set automatically. | | `children` | `ReactNode | ( (item: object )) => ReactElement` | Static child items or a function to render children. | | `dependencies` | `ReadonlyArray<any>` | Values that should invalidate the item cache when using dynamic collections. | | `items` | `Iterable<object>` | Item objects in the section. | | `className` | `string` | The CSS className for the element. | | `style` | `CSSProperties` | The inline style for the element. | Accessibility | Name | Type | Description | | --- | --- | --- | | `aria-label` | `string` | An accessibility label for the section. | ### Header# A `<Header>` defines the title for a `<ListBoxSection>`. It accepts all DOM attributes. ### ListBoxItem# A `<ListBoxItem>` defines a single option within a `<ListBox>`. If the `children` are not plain text, then the `textValue` prop must also be set to a plain text representation, which will be used for autocomplete in the ComboBox. Show props | Name | Type | Description | | --- | --- | --- | | `id` | `Key` | The unique id of the item. | | `value` | `object` | The object value that this item represents. When using dynamic collections, this is set automatically. | | `textValue` | `string` | A string representation of the item's contents, used for features like typeahead. | | `isDisabled` | `boolean` | Whether the item is disabled. | | `children` | `ReactNode | ( (values: ListBoxItemRenderProps & & { defaultChildren: ReactNode | | undefined } )) => ReactNode` | The children of the component. A function may be provided to alter the children based on component state. | | `className` | `string | ( (values: ListBoxItemRenderProps & & { defaultClassName: string | | undefined } )) => string` | The CSS className for the element. A function may be provided to compute the class based on component state. | | `style` | `CSSProperties | ( (values: ListBoxItemRenderProps & & { defaultStyle: CSSProperties } )) => CSSProperties | undefined` | The inline style for the element. A function may be provided to compute the style based on component state. | | `href` | ` Href ` | A URL to link to. See MDN. | | `hrefLang` | `string` | Hints at the human language of the linked URL. SeeMDN. | | `target` | `HTMLAttributeAnchorTarget` | The target window for the link. See MDN. | | `rel` | `string` | The relationship between the linked resource and the current page. See MDN. | | `download` | `boolean | string` | Causes the browser to download the linked URL. A string may be provided to suggest a file name. See MDN. | | `ping` | `string` | A space-separated list of URLs to ping when the link is followed. See MDN. | | `referrerPolicy` | `HTMLAttributeReferrerPolicy` | How much of the referrer to send when following the link. See MDN. | | `routerOptions` | ` RouterOptions ` | Options for the configured client side router. | Events | Name | Type | Description | | --- | --- | --- | | `onAction` | `() => void` | Handler that is called when a user performs an action on the item. The exact user event depends on the collection's `selectionBehavior` prop and the interaction modality. | | `onHoverStart` | `( (e: HoverEvent )) => void` | Handler that is called when a hover interaction starts. | | `onHoverEnd` | `( (e: HoverEvent )) => void` | Handler that is called when a hover interaction ends. | | `onHoverChange` | `( (isHovering: boolean )) => void` | Handler that is called when the hover state changes. | Accessibility | Name | Type | Description | | --- | --- | --- | | `aria-label` | `string` | An accessibility label for this item. | ### FieldError# A `<FieldError>` displays validation errors. Show props | Name | Type | Description | | --- | --- | --- | | `children` | `ReactNode | ( (values: FieldErrorRenderProps & & { defaultChildren: ReactNode | | undefined } )) => ReactNode` | The children of the component. A function may be provided to alter the children based on component state. | | `className` | `string | ( (values: FieldErrorRenderProps & & { defaultClassName: string | | undefined } )) => string` | The CSS className for the element. A function may be provided to compute the class based on component state. | | `style` | `CSSProperties | ( (values: FieldErrorRenderProps & & { defaultStyle: CSSProperties } )) => CSSProperties | undefined` | The inline style for the element. A function may be provided to compute the style based on component state. | Accessibility | Name | Type | Description | | --- | --- | --- | | `id` | `string` | The element's unique identifier. See MDN. | ## Styling# * * * React Aria components can be styled in many ways, including using CSS classes, inline styles, utility classes (e.g. Tailwind), CSS-in-JS (e.g. Styled Components), etc. By default, all components include a builtin `className` attribute which can be targeted using CSS selectors. These follow the `react-aria-ComponentName` naming convention. .react-aria-ComboBox { /* ... */ } .react-aria-ComboBox { /* ... */ } .react-aria-ComboBox { /* ... */ } A custom `className` can also be specified on any component. This overrides the default `className` provided by React Aria with your own. <ComboBox className="my-combobox"> {/* ... */} </ComboBox> <ComboBox className="my-combobox"> {/* ... */} </ComboBox> <ComboBox className="my-combobox"> {/* ... */} </ComboBox> In addition, some components support multiple UI states (e.g. pressed, hovered, etc.). React Aria components expose states using data attributes, which you can target in CSS selectors. 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] { /* ... */ } The `className` and `style` props also accept functions which receive states for styling. This lets you dynamically determine the classes or styles to apply, which is useful when using utility CSS libraries like Tailwind. <ListBoxItem className={({ isSelected }) => isSelected ? 'bg-blue-400' : 'bg-gray-100'} > Item </ListBoxItem> <ListBoxItem className={({ isSelected }) => isSelected ? 'bg-blue-400' : 'bg-gray-100'} > Item </ListBoxItem> <ListBoxItem className={( { isSelected } ) => isSelected ? 'bg-blue-400' : 'bg-gray-100'} > 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 />} Item </> )} </ListBoxItem> <ListBoxItem> {({isSelected}) => ( <> {isSelected && <CheckmarkIcon />} Item </> )} </ListBoxItem> <ListBoxItem> {( { isSelected } ) => ( <> {isSelected && ( <CheckmarkIcon /> )} Item </> )} </ListBoxItem> The states and selectors for each component used in a `ComboBox` are documented below. ### ComboBox# A `ComboBox` can be targeted with the `.react-aria-ComboBox` CSS selector, or by overriding with a custom `className`. It supports the following states: | Name | CSS Selector | Description | | --- | --- | --- | | `isOpen` | `[data-open]` | Whether the combobox is currently open. | | `isDisabled` | `[data-disabled]` | Whether the combobox is disabled. | | `isInvalid` | `[data-invalid]` | Whether the combobox is invalid. | | `isRequired` | `[data-required]` | Whether the combobox is required. | ### Label# A `Label` can be targeted with the `.react-aria-Label` CSS selector, or by overriding with a custom `className`. ### Input# An `Input` can be targeted with the `.react-aria-Input` CSS selector, or by overriding with a custom `className`. It supports the following states: | Name | CSS Selector | Description | | --- | --- | --- | | `isHovered` | `[data-hovered]` | Whether the input is currently hovered with a mouse. | | `isFocused` | `[data-focused]` | Whether the input is focused, either via a mouse or keyboard. | | `isFocusVisible` | `[data-focus-visible]` | Whether the input is keyboard focused. | | `isDisabled` | `[data-disabled]` | Whether the input is disabled. | | `isInvalid` | `[data-invalid]` | Whether the input is invalid. | ### Button# A Button can be targeted with the `.react-aria-Button` CSS selector, or by overriding with a custom `className`. It supports the following states: | Name | CSS Selector | Description | | --- | --- | --- | | `isHovered` | `[data-hovered]` | Whether the button is currently hovered with a mouse. | | `isPressed` | `[data-pressed]` | Whether the button is currently in a pressed state. | | `isFocused` | `[data-focused]` | Whether the button is focused, either via a mouse or keyboard. | | `isFocusVisible` | `[data-focus-visible]` | Whether the button is keyboard focused. | | `isDisabled` | `[data-disabled]` | Whether the button is disabled. | | `isPending` | `[data-pending]` | Whether the button is currently in a pending state. | ### Popover# The Popover component can be targeted with the `.react-aria-Popover` CSS selector, or by overriding with a custom `className`. Note that it renders in a React Portal, so it will not appear as a descendant of the ComboBox in the DOM. It supports the following states and render props: | Name | CSS Selector | Description | | --- | --- | --- | | `trigger` | `[data-trigger="..."]` | The name of the component that triggered the popover, e.g. "DialogTrigger" or "ComboBox". | | `placement` | `[data-placement="left | right | top | bottom"]` | The placement of the popover relative to the trigger. | | `isEntering` | `[data-entering]` | Whether the popover is currently entering. Use this to apply animations. | | `isExiting` | `[data-exiting]` | Whether the popover is currently exiting. Use this to apply animations. | Within a ComboBox, the popover will have the `data-trigger="ComboBox"` attribute, which can be used to define combobox-specific styles. In addition, the `--trigger-width` CSS custom property will be set on the popover, which you can use to make the popover match the width of the combobox. .react-aria-Popover[data-trigger=ComboBox] { width: var(--trigger-width); } .react-aria-Popover[data-trigger=ComboBox] { width: var(--trigger-width); } .react-aria-Popover[data-trigger=ComboBox] { width: var(--trigger-width); } ### ListBox# A ListBox can be targeted with the `.react-aria-ListBox` CSS selector, or by overriding with a custom `className`. ### ListBoxSection# A `ListBoxSection` can be targeted with the `.react-aria-ListBoxSection` CSS selector, or by overriding with a custom `className`. See sections for examples. ### Header# A `Header` within a `ListBoxSection` can be targeted with the `.react-aria-Header` CSS selector, or by overriding with a custom `className`. See sections for examples. ### ListBoxItem# A `ListBoxItem` can be targeted with the `.react-aria-ListBoxItem` CSS selector, or by overriding with a custom `className`. It supports the following states and render props: | Name | CSS Selector | Description | | --- | --- | --- | | `isHovered` | `[data-hovered]` | Whether the item is currently hovered with a mouse. | | `isPressed` | `[data-pressed]` | Whether the item is currently in a pressed state. | | `isSelected` | `[data-selected]` | Whether the item is currently selected. | | `isFocused` | `[data-focused]` | Whether the item is currently focused. | | `isFocusVisible` | `[data-focus-visible]` | Whether the item is currently keyboard focused. | | `isDisabled` | `[data-disabled]` | Whether the item is non-interactive, i.e. both selection and actions are disabled and the item may not be focused. Dependent on `disabledKeys` and `disabledBehavior`. | | `selectionMode` | `[data-selection-mode="single | multiple"]` | The type of selection that is allowed in the collection. | | `selectionBehavior` | `—` | The selection behavior for the collection. | Items also support two slots: a label, and a description. When provided using the `<Text>` element, the item will have `aria-labelledby` and `aria-describedby` attributes pointing to these slots, improving screen reader announcement. See text slots for an example. Note that items may not contain interactive children such as buttons, as screen readers will not be able to access them. ### Text# The help text elements within a `ComboBox` can be targeted with the `[slot=description]` and `[slot=errorMessage]` CSS selectors, or by adding a custom `className`. ### FieldError# A `FieldError` can be targeted with the `.react-aria-FieldError` CSS selector, or by overriding with a custom `className`. It supports the following render props: | Name | Description | | --- | --- | | `isInvalid` | Whether the input value is invalid. | | `validationErrors` | The current error messages for the input if it is invalid, otherwise an empty array. | | `validationDetails` | The native validation details for the input. | ## Advanced customization# * * * ### Composition# If you need to customize one of the components within a `ComboBox`, such as `Input` or `ListBox`, in many cases you can create a wrapper component. This lets you customize the props passed to the component. function MyListBox(props) { return <ListBox {...props} className="my-listbox" /> } function MyListBox(props) { return <ListBox {...props} className="my-listbox" /> } function MyListBox( props ) { return ( <ListBox {...props} className="my-listbox" /> ); } ### Contexts# All React Aria Components export a corresponding context that can be used to send props to them from a parent element. This enables you to build your own compositional APIs similar to those found in React Aria Components itself. You can send any prop or ref via context that you could pass to the corresponding component. The local props and ref on the component are merged with the ones passed via context, with the local props taking precedence (following the rules documented in mergeProps). | Component | Context | Props | Ref | | --- | --- | --- | --- | | `ComboBox` | `ComboBoxContext` | ` ComboBoxProps ` | `HTMLDivElement` | This example shows a `FieldGroup` component that renders a group of filters with a title. The entire group can be marked as disabled via the `isDisabled` prop, which is passed to all child selects via the `ComboBoxContext` provider. import {ComboBoxContext} from 'react-aria-components'; interface FieldGroupProps { title?: string, children?: React.ReactNode, isDisabled?: boolean } function FieldGroup({title, children, isDisabled}: FieldGroupProps) { return ( <fieldset> <legend>{title}</legend> <ComboBoxContext.Provider value={{isDisabled}}> {children} </ComboBoxContext.Provider> </fieldset> ); } <FieldGroup title="Filters" isDisabled> <MyComboBox label="Status" defaultSelectedKey="published"> <ListBoxItem id="draft">Draft</ListBoxItem> <ListBoxItem id="published">Published</ListBoxItem> <ListBoxItem id="deleted">Deleted</ListBoxItem> </MyComboBox> <MyComboBox label="Author" defaultSelectedKey="emma"> <ListBoxItem id="john">John</ListBoxItem> <ListBoxItem id="emma">Emma</ListBoxItem> <ListBoxItem id="tim">Tim</ListBoxItem> </MyComboBox> </FieldGroup> import {ComboBoxContext} from 'react-aria-components'; interface FieldGroupProps { title?: string; children?: React.ReactNode; isDisabled?: boolean; } function FieldGroup( { title, children, isDisabled }: FieldGroupProps ) { return ( <fieldset> <legend>{title}</legend> <ComboBoxContext.Provider value={{ isDisabled }}> {children} </ComboBoxContext.Provider> </fieldset> ); } <FieldGroup title="Filters" isDisabled> <MyComboBox label="Status" defaultSelectedKey="published" > <ListBoxItem id="draft">Draft</ListBoxItem> <ListBoxItem id="published">Published</ListBoxItem> <ListBoxItem id="deleted">Deleted</ListBoxItem> </MyComboBox> <MyComboBox label="Author" defaultSelectedKey="emma"> <ListBoxItem id="john">John</ListBoxItem> <ListBoxItem id="emma">Emma</ListBoxItem> <ListBoxItem id="tim">Tim</ListBoxItem> </MyComboBox> </FieldGroup> import {ComboBoxContext} from 'react-aria-components'; interface FieldGroupProps { title?: string; children?: React.ReactNode; isDisabled?: boolean; } function FieldGroup( { title, children, isDisabled }: FieldGroupProps ) { return ( <fieldset> <legend> {title} </legend> <ComboBoxContext.Provider value={{ isDisabled }} > {children} </ComboBoxContext.Provider> </fieldset> ); } <FieldGroup title="Filters" isDisabled > <MyComboBox label="Status" defaultSelectedKey="published" > <ListBoxItem id="draft"> Draft </ListBoxItem> <ListBoxItem id="published"> Published </ListBoxItem> <ListBoxItem id="deleted"> Deleted </ListBoxItem> </MyComboBox> <MyComboBox label="Author" defaultSelectedKey="emma" > <ListBoxItem id="john"> John </ListBoxItem> <ListBoxItem id="emma"> Emma </ListBoxItem> <ListBoxItem id="tim"> Tim </ListBoxItem> </MyComboBox> </FieldGroup> Filters Status ▼ Author ▼ Show CSS fieldset { padding: 1.5em; width: fit-content; } fieldset { padding: 1.5em; width: fit-content; } fieldset { padding: 1.5em; width: fit-content; } ### Custom children# ComboBox passes props to its child components, such as the label and input, via their associated contexts. These contexts are exported so you can also consume them in your own custom components. This enables you to reuse existing components from your app or component library together with React Aria Components. | Component | Context | Props | Ref | | --- | --- | --- | --- | | `Label` | `LabelContext` | ` LabelProps ` | `HTMLLabelElement` | | `Button` | `ButtonContext` | ` ButtonProps ` | `HTMLButtonElement` | | `Input` | `InputContext` | ` InputProps ` | `HTMLInputElement` | | `Popover` | `PopoverContext` | ` PopoverProps ` | `HTMLElement` | | `ListBox` | `ListBoxContext` | ` ListBoxProps ` | `HTMLDivElement` | | `Text` | `TextContext` | ` TextProps ` | `HTMLElement` | This example consumes from `LabelContext` in an existing styled label component to make it compatible with React Aria Components. The `useContextProps` hook merges the local props and ref with the ones provided via context by ComboBox. import type {LabelProps} from 'react-aria-components'; import {LabelContext, useContextProps} from 'react-aria-components'; const MyCustomLabel = React.forwardRef( (props: LabelProps, ref: React.ForwardedRef<HTMLLabelElement>) => { // Merge the local props and ref with the ones provided via context. [props, ref] = useContextProps(props, ref, LabelContext); // ... your existing Label component return <label {...props} ref={ref} />; } ); import type {LabelProps} from 'react-aria-components'; import { LabelContext, useContextProps } from 'react-aria-components'; const MyCustomLabel = React.forwardRef( ( props: LabelProps, ref: React.ForwardedRef<HTMLLabelElement> ) => { // Merge the local props and ref with the ones provided via context. [props, ref] = useContextProps( props, ref, LabelContext ); // ... your existing Label component return <label {...props} ref={ref} />; } ); import type {LabelProps} from 'react-aria-components'; import { LabelContext, useContextProps } from 'react-aria-components'; const MyCustomLabel = React.forwardRef( ( props: LabelProps, ref: React.ForwardedRef< HTMLLabelElement > ) => { // Merge the local props and ref with the ones provided via context. [props, ref] = useContextProps( props, ref, LabelContext ); // ... your existing Label component return ( <label {...props} ref={ref} /> ); } ); Now you can use `MyCustomLabel` within a `ComboBox`, in place of the builtin React Aria Components `Label`. <ComboBox> <MyCustomLabel>Name</MyCustomLabel> {/* ... */} </ComboBox> <ComboBox> <MyCustomLabel>Name</MyCustomLabel> {/* ... */} </ComboBox> <ComboBox> <MyCustomLabel> Name </MyCustomLabel> {/* ... */} </ComboBox> ### State# ComboBox provides an `ComboBoxState` object to its children via `ComboBoxStateContext`. This can be used to access and manipulate the combo box's state. This example shows a `ComboBoxClearButton` component that can be placed within a `ComboBox` to allow the user to clear the selected value. import {ComboBoxStateContext} from 'react-aria-components'; function ComboBoxClearButton() { let state = React.useContext(ComboBoxStateContext); return ( <Button // Don't inherit default Button behavior from ComboBox. slot={null} className="clear-button" aria-label="Clear" onPress={() => state?.setSelectedKey(null)}> ✕ </Button> ); } <ComboBox defaultSelectedKey="cat"> <Label>Favorite Animal</Label> <div> <Input /> <ComboBoxClearButton /> <Button>▼</Button> </div> <Popover> <ListBox> <ListBoxItem id="cat">Cat</ListBoxItem> <ListBoxItem id="dog">Dog</ListBoxItem> <ListBoxItem id="kangaroo">Kangaroo</ListBoxItem> </ListBox> </Popover> </ComboBox> import {ComboBoxStateContext} from 'react-aria-components'; function ComboBoxClearButton() { let state = React.useContext(ComboBoxStateContext); return ( <Button // Don't inherit default Button behavior from ComboBox. slot={null} className="clear-button" aria-label="Clear" onPress={() => state?.setSelectedKey(null)} > ✕ </Button> ); } <ComboBox defaultSelectedKey="cat"> <Label>Favorite Animal</Label> <div> <Input /> <ComboBoxClearButton /> <Button>▼</Button> </div> <Popover> <ListBox> <ListBoxItem id="cat">Cat</ListBoxItem> <ListBoxItem id="dog">Dog</ListBoxItem> <ListBoxItem id="kangaroo">Kangaroo</ListBoxItem> </ListBox> </Popover> </ComboBox> import {ComboBoxStateContext} from 'react-aria-components'; function ComboBoxClearButton() { let state = React .useContext( ComboBoxStateContext ); return ( <Button // Don't inherit default Button behavior from ComboBox. slot={null} className="clear-button" aria-label="Clear" onPress={() => state ?.setSelectedKey( null )} > ✕ </Button> ); } <ComboBox defaultSelectedKey="cat"> <Label> Favorite Animal </Label> <div> <Input /> <ComboBoxClearButton /> <Button>▼</Button> </div> <Popover> <ListBox> <ListBoxItem id="cat"> Cat </ListBoxItem> <ListBoxItem id="dog"> Dog </ListBoxItem> <ListBoxItem id="kangaroo"> Kangaroo </ListBoxItem> </ListBox> </Popover> </ComboBox> Favorite Animal ✕▼ Show CSS .clear-button { width: 1.143rem; height: 1.143rem; border-radius: 1.143rem; margin-left: -3.143rem; font-size: 0.857rem; line-height: 0.857rem; vertical-align: middle; text-align: center; background: gray; color: white; border: none; padding: 0; outline: none; &[data-pressed] { background: dimgray; } &[data-focus-visible] { outline: 2px solid var(--focus-ring-color); outline-offset: 2px; } + .react-aria-Button { margin-left: 4px; } } .clear-button { width: 1.143rem; height: 1.143rem; border-radius: 1.143rem; margin-left: -3.143rem; font-size: 0.857rem; line-height: 0.857rem; vertical-align: middle; text-align: center; background: gray; color: white; border: none; padding: 0; outline: none; &[data-pressed] { background: dimgray; } &[data-focus-visible] { outline: 2px solid var(--focus-ring-color); outline-offset: 2px; } + .react-aria-Button { margin-left: 4px; } } .clear-button { width: 1.143rem; height: 1.143rem; border-radius: 1.143rem; margin-left: -3.143rem; font-size: 0.857rem; line-height: 0.857rem; vertical-align: middle; text-align: center; background: gray; color: white; border: none; padding: 0; outline: none; &[data-pressed] { background: dimgray; } &[data-focus-visible] { outline: 2px solid var(--focus-ring-color); outline-offset: 2px; } + .react-aria-Button { margin-left: 4px; } } ### Hooks# If you need to customize things even further, such as accessing internal state, intercepting events, or customizing the DOM structure, you can drop down to the lower level Hook-based API. See useComboBox for more details. ## Testing# * * * ### Test utils alpha# `@react-aria/test-utils` offers common combobox interaction utilities which you may find helpful when writing tests. See here for more information on how to setup these utilities in your tests. Below is the full definition of the combobox tester and a sample of how you could use it in your test suite. // Combobox.test.ts import {render} from '@testing-library/react'; import {User} from '@react-aria/test-utils'; let testUtilUser = new User({ interactionType: 'mouse' }); // ... it('ComboBox can select an option via keyboard', async function () { // Render your test component/app and initialize the combobox tester let { getByTestId } = render( <ComboBox data-testid="test-combobox"> ... </ComboBox> ); let comboboxTester = testUtilUser.createTester('ComboBox', { root: getByTestId('test-combobox'), interactionType: 'keyboard' }); await comboboxTester.open(); expect(comboboxTester.listbox).toBeInTheDocument(); let options = comboboxTester.options(); await comboboxTester.selectOption({ option: options[0] }); expect(comboboxTester.combobox.value).toBe('One'); expect(comboboxTester.listbox).not.toBeInTheDocument(); }); // Combobox.test.ts import {render} from '@testing-library/react'; import {User} from '@react-aria/test-utils'; let testUtilUser = new User({ interactionType: 'mouse' }); // ... it('ComboBox can select an option via keyboard', async function () { // Render your test component/app and initialize the combobox tester let { getByTestId } = render( <ComboBox data-testid="test-combobox"> ... </ComboBox> ); let comboboxTester = testUtilUser.createTester( 'ComboBox', { root: getByTestId('test-combobox'), interactionType: 'keyboard' } ); await comboboxTester.open(); expect(comboboxTester.listbox).toBeInTheDocument(); let options = comboboxTester.options(); await comboboxTester.selectOption({ option: options[0] }); expect(comboboxTester.combobox.value).toBe('One'); expect(comboboxTester.listbox).not.toBeInTheDocument(); }); // Combobox.test.ts import {render} from '@testing-library/react'; import {User} from '@react-aria/test-utils'; let testUtilUser = new User({ interactionType: 'mouse' }); // ... it('ComboBox can select an option via keyboard', async function () { // Render your test component/app and initialize the combobox tester let { getByTestId } = render( <ComboBox data-testid="test-combobox"> ... </ComboBox> ); let comboboxTester = testUtilUser .createTester( 'ComboBox', { root: getByTestId( 'test-combobox' ), interactionType: 'keyboard' } ); await comboboxTester .open(); expect( comboboxTester .listbox ).toBeInTheDocument(); let options = comboboxTester .options(); await comboboxTester .selectOption({ option: options[0] }); expect( comboboxTester .combobox.value ).toBe('One'); expect( comboboxTester .listbox ).not .toBeInTheDocument(); }); ### Properties | Name | Type | Description | | --- | --- | --- | | `combobox` | `HTMLElement` | Returns the combobox. | | `trigger` | `HTMLElement` | Returns the combobox trigger button. | | `listbox` | `HTMLElement | null` | Returns the combobox's listbox if present. | | `sections` | `HTMLElement[]` | Returns the combobox's sections if present. | | `focusedOption` | `HTMLElement | null` | Returns the currently focused option in the combobox's dropdown if any. | ### Methods | Method | Description | | --- | --- | | `constructor( (opts: ComboBoxTesterOpts )): void` | | | `setInteractionType( (type: UserOpts ['interactionType'] )): void` | Set the interaction type used by the combobox tester. | | `open( (opts: ComboBoxOpenOpts )): Promise<void>` | Opens the combobox dropdown. Defaults to using the interaction type set on the combobox tester. | | `findOption( (opts: { optionIndexOrText: number | | string } )): HTMLElement` | Returns an option matching the specified index or text content. | | `selectOption( (opts: ComboBoxSelectOpts )): Promise<void>` | Selects the desired combobox option. Defaults to using the interaction type set on the combobox tester. If necessary, will open the combobox dropdown beforehand. The desired option can be targeted via the option's node, the option's text, or the option's index. | | `close(): Promise<void>` | Closes the combobox dropdown. | | `options( (opts: { element?: HTMLElement } )): HTMLElement[]` | Returns the combobox's options if present. Can be filtered to a subsection of the listbox if provided via `element`. | --- ## Page: https://react-spectrum.adobe.com/react-aria/Select.html # Select A select displays a collapsible list of options and allows a user to select one of them. <table><tbody><tr><th>install</th><td><code>yarn add react-aria-components</code></td></tr><tr><th>version</th><td>1.10.1</td></tr><tr><th>usage</th><td><code><span>import</span> {Select} <span>from</span> <span>'react-aria-components'</span></code></td></tr></tbody></table> View ARIA pattern W3C View repository GitHub View package NPM ## Example# * * * import {Button, Label, ListBox, ListBoxItem, Popover, Select, SelectValue} from 'react-aria-components'; <Select> <Label>Favorite Animal</Label> <Button> <SelectValue /> <span aria-hidden="true">▼</span> </Button> <Popover> <ListBox> <ListBoxItem>Aardvark</ListBoxItem> <ListBoxItem>Cat</ListBoxItem> <ListBoxItem>Dog</ListBoxItem> <ListBoxItem>Kangaroo</ListBoxItem> <ListBoxItem>Panda</ListBoxItem> <ListBoxItem>Snake</ListBoxItem> </ListBox> </Popover> </Select> import { Button, Label, ListBox, ListBoxItem, Popover, Select, SelectValue } from 'react-aria-components'; <Select> <Label>Favorite Animal</Label> <Button> <SelectValue /> <span aria-hidden="true">▼</span> </Button> <Popover> <ListBox> <ListBoxItem>Aardvark</ListBoxItem> <ListBoxItem>Cat</ListBoxItem> <ListBoxItem>Dog</ListBoxItem> <ListBoxItem>Kangaroo</ListBoxItem> <ListBoxItem>Panda</ListBoxItem> <ListBoxItem>Snake</ListBoxItem> </ListBox> </Popover> </Select> import { Button, Label, ListBox, ListBoxItem, Popover, Select, SelectValue } from 'react-aria-components'; <Select> <Label> Favorite Animal </Label> <Button> <SelectValue /> <span aria-hidden="true"> ▼ </span> </Button> <Popover> <ListBox> <ListBoxItem> Aardvark </ListBoxItem> <ListBoxItem> Cat </ListBoxItem> <ListBoxItem> Dog </ListBoxItem> <ListBoxItem> Kangaroo </ListBoxItem> <ListBoxItem> Panda </ListBoxItem> <ListBoxItem> Snake </ListBoxItem> </ListBox> </Popover> </Select> Favorite AnimalSelect an item▼ AardvarkCatDogKangarooPandaSnake Show CSS @import "@react-aria/example-theme"; .react-aria-Select { color: var(--text-color); .react-aria-Button { box-shadow: 0 1px 2px rgba(0 0 0 / 0.1); border-radius: 6px; font-size: 1.072rem; padding: 0.286rem 0.286rem 0.286rem 0.571rem; display: flex; align-items: center; max-width: 250px; &[data-focus-visible] { outline: 2px solid var(--focus-ring-color); outline-offset: -1px; } } .react-aria-SelectValue { &[data-placeholder] { font-style: italic; color: var(--text-color-placeholder); } } span[aria-hidden] { width: 1.5rem; line-height: 1.375rem; margin-left: 1rem; padding: 1px; background: var(--highlight-background); color: var(--highlight-foreground); forced-color-adjust: none; border-radius: 4px; font-size: 0.857rem; } } .react-aria-Popover[data-trigger=Select] { min-width: var(--trigger-width); .react-aria-ListBox { display: block; width: unset; max-height: inherit; min-height: unset; border: none; .react-aria-Header { padding-left: 1.571rem; } } .react-aria-ListBoxItem { padding: 0 0.571rem 0 1.571rem; &[data-focus-visible] { outline: none; } &[data-selected] { font-weight: 600; background: unset; color: var(--text-color); &::before { content: '✓'; content: '✓' / ''; alt: ' '; position: absolute; top: 4px; left: 4px; } } &[data-focused], &[data-pressed] { background: var(--highlight-background); color: var(--highlight-foreground); } } } @import "@react-aria/example-theme"; .react-aria-Select { color: var(--text-color); .react-aria-Button { box-shadow: 0 1px 2px rgba(0 0 0 / 0.1); border-radius: 6px; font-size: 1.072rem; padding: 0.286rem 0.286rem 0.286rem 0.571rem; display: flex; align-items: center; max-width: 250px; &[data-focus-visible] { outline: 2px solid var(--focus-ring-color); outline-offset: -1px; } } .react-aria-SelectValue { &[data-placeholder] { font-style: italic; color: var(--text-color-placeholder); } } span[aria-hidden] { width: 1.5rem; line-height: 1.375rem; margin-left: 1rem; padding: 1px; background: var(--highlight-background); color: var(--highlight-foreground); forced-color-adjust: none; border-radius: 4px; font-size: 0.857rem; } } .react-aria-Popover[data-trigger=Select] { min-width: var(--trigger-width); .react-aria-ListBox { display: block; width: unset; max-height: inherit; min-height: unset; border: none; .react-aria-Header { padding-left: 1.571rem; } } .react-aria-ListBoxItem { padding: 0 0.571rem 0 1.571rem; &[data-focus-visible] { outline: none; } &[data-selected] { font-weight: 600; background: unset; color: var(--text-color); &::before { content: '✓'; content: '✓' / ''; alt: ' '; position: absolute; top: 4px; left: 4px; } } &[data-focused], &[data-pressed] { background: var(--highlight-background); color: var(--highlight-foreground); } } } @import "@react-aria/example-theme"; .react-aria-Select { color: var(--text-color); .react-aria-Button { box-shadow: 0 1px 2px rgba(0 0 0 / 0.1); border-radius: 6px; font-size: 1.072rem; padding: 0.286rem 0.286rem 0.286rem 0.571rem; display: flex; align-items: center; max-width: 250px; &[data-focus-visible] { outline: 2px solid var(--focus-ring-color); outline-offset: -1px; } } .react-aria-SelectValue { &[data-placeholder] { font-style: italic; color: var(--text-color-placeholder); } } span[aria-hidden] { width: 1.5rem; line-height: 1.375rem; margin-left: 1rem; padding: 1px; background: var(--highlight-background); color: var(--highlight-foreground); forced-color-adjust: none; border-radius: 4px; font-size: 0.857rem; } } .react-aria-Popover[data-trigger=Select] { min-width: var(--trigger-width); .react-aria-ListBox { display: block; width: unset; max-height: inherit; min-height: unset; border: none; .react-aria-Header { padding-left: 1.571rem; } } .react-aria-ListBoxItem { padding: 0 0.571rem 0 1.571rem; &[data-focus-visible] { outline: none; } &[data-selected] { font-weight: 600; background: unset; color: var(--text-color); &::before { content: '✓'; content: '✓' / ''; alt: ' '; position: absolute; top: 4px; left: 4px; } } &[data-focused], &[data-pressed] { background: var(--highlight-background); color: var(--highlight-foreground); } } } ## Features# * * * A select can be built using the <select> and <option> HTML elements, but this is not possible to style consistently cross browser, especially the options. `Select` helps achieve accessible select components that can be styled as needed without compromising on high quality interactions. * **Flexible** – Support for controlled and uncontrolled state, async loading, disabled items, validation, sections, complex items, and more. * **Keyboard navigation** – Select can be opened and navigated using the arrow keys, along with page up/down, home/end, etc. Auto scrolling, and typeahead both in the listbox and on the button, are supported as well. * **Accessible** – Follows the ARIA listbox pattern, with support for items and sections, and slots for label and description elements within each item for improved screen reader announcement. * **HTML form integration** – A visually hidden `<select>` element is included to enable HTML form integration and autofill. * **Validation** – Support for native HTML constraint validation with customizable UI, custom validation functions, and server-side validation errors. * **Styleable** – Items include builtin states for styling, such as hover, press, focus, selected, and disabled. ## Anatomy# * * * A select consists of a label, a button which displays a selected value, and a listbox, displayed in a popover. Users can click, touch, or use the keyboard on the button to open the listbox popover. `Select` also supports optional description and error message elements, which can be used to provide more context about the field, and any validation messages. These are linked with the input via the `aria-describedby` attribute. import {Button, FieldError, Header, Label, ListBox, ListBoxItem, ListBoxSection, Popover, Select, SelectValue, Text} from 'react-aria-components'; <Select> <Label /> <Button> <SelectValue /> </Button> <Text slot="description" /> <FieldError /> <Popover> <ListBox> <ListBoxItem> <Text slot="label" /> <Text slot="description" /> </ListBoxItem> <ListBoxSection> <Header /> <ListBoxItem /> </ListBoxSection> </ListBox> </Popover> </Select> import { Button, FieldError, Header, Label, ListBox, ListBoxItem, ListBoxSection, Popover, Select, SelectValue, Text } from 'react-aria-components'; <Select> <Label /> <Button> <SelectValue /> </Button> <Text slot="description" /> <FieldError /> <Popover> <ListBox> <ListBoxItem> <Text slot="label" /> <Text slot="description" /> </ListBoxItem> <ListBoxSection> <Header /> <ListBoxItem /> </ListBoxSection> </ListBox> </Popover> </Select> import { Button, FieldError, Header, Label, ListBox, ListBoxItem, ListBoxSection, Popover, Select, SelectValue, Text } from 'react-aria-components'; <Select> <Label /> <Button> <SelectValue /> </Button> <Text slot="description" /> <FieldError /> <Popover> <ListBox> <ListBoxItem> <Text slot="label" /> <Text slot="description" /> </ListBoxItem> <ListBoxSection> <Header /> <ListBoxItem /> </ListBoxSection> </ListBox> </Popover> </Select> If a select does not have a visible label, an `aria-label` or `aria-labelledby` prop must be passed instead to identify it to assistive technology. ### Concepts# `Select` makes use of the following concepts: Collections Defining collections of items, async loading, and updating items over time. Selection Interactions and data structures to represent selection. Forms Validating and submitting form data, and integrating with form libraries. ### Composed components# A `Select` uses the following components, which may also be used standalone or reused in other components. Label A label provides context for an input element. Button A button allows a user to perform an action. Popover A popover displays content in context with a trigger element. ListBox A listbox allows a user to select one or more options from a list. ## Examples# * * * Searchable Select A Select component with Autocomplete filtering. Status Select An issue status dropdown styled with Tailwind CSS. ## Starter kits# * * * To help kick-start your project, we offer starter kits that include example implementations of all React Aria components with various styling solutions. All components are fully styled, including support for dark mode, high contrast mode, and all UI states. Each starter comes with a pre-configured Storybook that you can experiment with, or use as a starting point for your own component library. Vanilla CSS Download ZIP Preview Tailwind CSS Download ZIP Preview ## Reusable wrappers# * * * If you will use a Select in multiple places in your app, you can wrap all of the pieces into a reusable component. This way, the DOM structure, styling code, and other logic are defined in a single place and reused everywhere to ensure consistency. This example wraps `Select` and all of its children together into a single component which accepts a `label` prop and `children`, which are passed through to the right places. It also shows how to use the `description` slot to render help text, and `FieldError` component to render validation errors. The `Item` component is also wrapped to apply class names based on the current state, as described in the styling section. import type {ListBoxItemProps, SelectProps, ValidationResult} from 'react-aria-components'; import {FieldError, Text} from 'react-aria-components'; interface MySelectProps<T extends object> extends Omit<SelectProps<T>, 'children'> { label?: string; description?: string; errorMessage?: string | ((validation: ValidationResult) => string); items?: Iterable<T>; children: React.ReactNode | ((item: T) => React.ReactNode); } export function MySelect<T extends object>( { label, description, errorMessage, children, items, ...props }: MySelectProps<T> ) { return ( <Select {...props}> <Label>{label}</Label> <Button> <SelectValue /> <span aria-hidden="true">▼</span> </Button> {description && <Text slot="description">{description}</Text>} <FieldError>{errorMessage}</FieldError> <Popover> <ListBox items={items}> {children} </ListBox> </Popover> </Select> ); } export function MyItem(props: ListBoxItemProps) { return ( <ListBoxItem {...props} className={({ isFocused, isSelected }) => `my-item ${isFocused ? 'focused' : ''} ${isSelected ? 'selected' : ''}`} /> ); } <MySelect label="Ice cream flavor"> <MyItem>Chocolate</MyItem> <MyItem>Mint</MyItem> <MyItem>Strawberry</MyItem> <MyItem>Vanilla</MyItem> </MySelect> import type { ListBoxItemProps, SelectProps, ValidationResult } from 'react-aria-components'; import {FieldError, Text} from 'react-aria-components'; interface MySelectProps<T extends object> extends Omit<SelectProps<T>, 'children'> { label?: string; description?: string; errorMessage?: | string | ((validation: ValidationResult) => string); items?: Iterable<T>; children: | React.ReactNode | ((item: T) => React.ReactNode); } export function MySelect<T extends object>( { label, description, errorMessage, children, items, ...props }: MySelectProps<T> ) { return ( <Select {...props}> <Label>{label}</Label> <Button> <SelectValue /> <span aria-hidden="true">▼</span> </Button> {description && ( <Text slot="description">{description}</Text> )} <FieldError>{errorMessage}</FieldError> <Popover> <ListBox items={items}> {children} </ListBox> </Popover> </Select> ); } export function MyItem(props: ListBoxItemProps) { return ( <ListBoxItem {...props} className={({ isFocused, isSelected }) => `my-item ${isFocused ? 'focused' : ''} ${ isSelected ? 'selected' : '' }`} /> ); } <MySelect label="Ice cream flavor"> <MyItem>Chocolate</MyItem> <MyItem>Mint</MyItem> <MyItem>Strawberry</MyItem> <MyItem>Vanilla</MyItem> </MySelect> import type { ListBoxItemProps, SelectProps, ValidationResult } from 'react-aria-components'; import { FieldError, Text } from 'react-aria-components'; interface MySelectProps< T extends object > extends Omit< SelectProps<T>, 'children' > { label?: string; description?: string; errorMessage?: | string | (( validation: ValidationResult ) => string); items?: Iterable<T>; children: | React.ReactNode | (( item: T ) => React.ReactNode); } export function MySelect< T extends object >({ label, description, errorMessage, children, items, ...props }: MySelectProps<T>) { return ( <Select {...props}> <Label> {label} </Label> <Button> <SelectValue /> <span aria-hidden="true"> ▼ </span> </Button> {description && ( <Text slot="description"> {description} </Text> )} <FieldError> {errorMessage} </FieldError> <Popover> <ListBox items={items} > {children} </ListBox> </Popover> </Select> ); } export function MyItem( props: ListBoxItemProps ) { return ( <ListBoxItem {...props} className={( { isFocused, isSelected } ) => `my-item ${ isFocused ? 'focused' : '' } ${ isSelected ? 'selected' : '' }`} /> ); } <MySelect label="Ice cream flavor"> <MyItem> Chocolate </MyItem> <MyItem>Mint</MyItem> <MyItem> Strawberry </MyItem> <MyItem> Vanilla </MyItem> </MySelect> Ice cream flavorSelect an item▼ ChocolateMintStrawberryVanilla Show CSS .my-item { margin: 2px; padding: 4px 8px 4px 22px; border-radius: 6px; outline: none; cursor: default; color: var(--text-color); font-size: 1.072rem; position: relative; &.selected { font-weight: 600; background: none; &::before { content: '✓'; content: '✓' / ''; alt: ' '; position: absolute; top: 4px; left: 4px; } } &.focused { background: #e70073; color: white; } } @media (forced-colors: active) { .my-item.focused { background: Highlight; color: HighlightText; } } .my-item { margin: 2px; padding: 4px 8px 4px 22px; border-radius: 6px; outline: none; cursor: default; color: var(--text-color); font-size: 1.072rem; position: relative; &.selected { font-weight: 600; background: none; &::before { content: '✓'; content: '✓' / ''; alt: ' '; position: absolute; top: 4px; left: 4px; } } &.focused { background: #e70073; color: white; } } @media (forced-colors: active) { .my-item.focused { background: Highlight; color: HighlightText; } } .my-item { margin: 2px; padding: 4px 8px 4px 22px; border-radius: 6px; outline: none; cursor: default; color: var(--text-color); font-size: 1.072rem; position: relative; &.selected { font-weight: 600; background: none; &::before { content: '✓'; content: '✓' / ''; alt: ' '; position: absolute; top: 4px; left: 4px; } } &.focused { background: #e70073; color: white; } } @media (forced-colors: active) { .my-item.focused { background: Highlight; color: HighlightText; } } ## Content# * * * `Select` follows the Collection Components API, accepting both static and dynamic collections. The examples above show static collections, which can be used when the full list of options is known ahead of time. Dynamic collections, as shown below, can be used when the options come from an external data source such as an API call, or update over time. As seen below, an iterable list of options is passed to the Select using the `items` prop. Each item accepts an `id` prop, which is passed to the `onSelectionChange` handler to identify the selected item. Alternatively, if the item objects contain an `id` property, as shown in the example below, then this is used automatically and an `id` prop is not required. function Example() { let options = [ {id: 1, name: 'Aerospace'}, {id: 2, name: 'Mechanical'}, {id: 3, name: 'Civil'}, {id: 4, name: 'Biomedical'}, {id: 5, name: 'Nuclear'}, {id: 6, name: 'Industrial'}, {id: 7, name: 'Chemical'}, {id: 8, name: 'Agricultural'}, {id: 9, name: 'Electrical'} ]; return ( <MySelect label="Pick an engineering major" items={options}> {(item) => <ListBoxItem>{item.name}</ListBoxItem>} </MySelect> ); } function Example() { let options = [ { id: 1, name: 'Aerospace' }, { id: 2, name: 'Mechanical' }, { id: 3, name: 'Civil' }, { id: 4, name: 'Biomedical' }, { id: 5, name: 'Nuclear' }, { id: 6, name: 'Industrial' }, { id: 7, name: 'Chemical' }, { id: 8, name: 'Agricultural' }, { id: 9, name: 'Electrical' } ]; return ( <MySelect label="Pick an engineering major" items={options} > {(item) => <ListBoxItem>{item.name}</ListBoxItem>} </MySelect> ); } function Example() { let options = [ { id: 1, name: 'Aerospace' }, { id: 2, name: 'Mechanical' }, { id: 3, name: 'Civil' }, { id: 4, name: 'Biomedical' }, { id: 5, name: 'Nuclear' }, { id: 6, name: 'Industrial' }, { id: 7, name: 'Chemical' }, { id: 8, name: 'Agricultural' }, { id: 9, name: 'Electrical' } ]; return ( <MySelect label="Pick an engineering major" items={options} > {(item) => ( <ListBoxItem> {item.name} </ListBoxItem> )} </MySelect> ); } Pick an engineering majorSelect an item▼ AerospaceMechanicalCivilBiomedicalNuclearIndustrialChemicalAgriculturalElectrical ## Selection# * * * Setting a selected option can be done by using the `defaultSelectedKey` or `selectedKey` prop. The selected key corresponds to the `id` prop of an item. When `Select` is used with a dynamic collection as described above, the id of each item is derived from the data. See the Selection guide for more details. import type {Key} from 'react-aria-components'; function Example() { let options = [ {name: 'Koala'}, {name: 'Kangaroo'}, {name: 'Platypus'}, {name: 'Bald Eagle'}, {name: 'Bison'}, {name: 'Skunk'} ]; let [animal, setAnimal] = React.useState<Key>("Bison"); return ( <MySelect label="Pick an animal (controlled)" items={options} selectedKey={animal} onSelectionChange={selected => setAnimal(selected)}> {item => <ListBoxItem id={item.name}>{item.name}</ListBoxItem>} </MySelect> ); } import type {Key} from 'react-aria-components'; function Example() { let options = [ { name: 'Koala' }, { name: 'Kangaroo' }, { name: 'Platypus' }, { name: 'Bald Eagle' }, { name: 'Bison' }, { name: 'Skunk' } ]; let [animal, setAnimal] = React.useState<Key>('Bison'); return ( <MySelect label="Pick an animal (controlled)" items={options} selectedKey={animal} onSelectionChange={(selected) => setAnimal(selected)} > {(item) => ( <ListBoxItem id={item.name}> {item.name} </ListBoxItem> )} </MySelect> ); } import type {Key} from 'react-aria-components'; function Example() { let options = [ { name: 'Koala' }, { name: 'Kangaroo' }, { name: 'Platypus' }, { name: 'Bald Eagle' }, { name: 'Bison' }, { name: 'Skunk' } ]; let [ animal, setAnimal ] = React.useState< Key >('Bison'); return ( <MySelect label="Pick an animal (controlled)" items={options} selectedKey={animal} onSelectionChange={(selected) => setAnimal( selected )} > {(item) => ( <ListBoxItem id={item.name} > {item.name} </ListBoxItem> )} </MySelect> ); } Pick an animal (controlled)Bison▼ KoalaKangarooPlatypusBald EagleBisonSkunk ### HTML forms# Select supports the `name` prop for integration with HTML forms. The `id` of the selected item will be submitted to the server. <MySelect label="Favorite Animal" name="favoriteAnimalId"> <ListBoxItem id="panda">Panda</ListBoxItem> <ListBoxItem id="cat">Cat</ListBoxItem> <ListBoxItem id="dog">Dog</ListBoxItem> </MySelect> <MySelect label="Favorite Animal" name="favoriteAnimalId"> <ListBoxItem id="panda">Panda</ListBoxItem> <ListBoxItem id="cat">Cat</ListBoxItem> <ListBoxItem id="dog">Dog</ListBoxItem> </MySelect> <MySelect label="Favorite Animal" name="favoriteAnimalId"> <ListBoxItem id="panda"> Panda </ListBoxItem> <ListBoxItem id="cat"> Cat </ListBoxItem> <ListBoxItem id="dog"> Dog </ListBoxItem> </MySelect> Favorite AnimalSelect an item▼ PandaCatDog ## Links# * * * By default, interacting with an item in a Select triggers `onSelectionChange`. Alternatively, items may be links to another page or website. This can be achieved by passing the `href` prop to the `<ListBoxItem>` component. Link items in a `Select` are not selectable. <MySelect label="Project"> <ListBoxItem href="https://example.com/" target="_blank"> Create new… </ListBoxItem> <ListBoxItem>Proposal</ListBoxItem> <ListBoxItem>Budget</ListBoxItem> <ListBoxItem>Onboarding</ListBoxItem> </MySelect> <MySelect label="Project"> <ListBoxItem href="https://example.com/" target="_blank"> Create new… </ListBoxItem> <ListBoxItem>Proposal</ListBoxItem> <ListBoxItem>Budget</ListBoxItem> <ListBoxItem>Onboarding</ListBoxItem> </MySelect> <MySelect label="Project"> <ListBoxItem href="https://example.com/" target="_blank" > Create new… </ListBoxItem> <ListBoxItem> Proposal </ListBoxItem> <ListBoxItem> Budget </ListBoxItem> <ListBoxItem> Onboarding </ListBoxItem> </MySelect> ProjectSelect an item▼ Create new…ProposalBudgetOnboarding ### Client side routing# The `<ListBoxItem>` component works with frameworks and client side routers like Next.js and React Router. As with other React Aria components that support links, this works via the `RouterProvider` component at the root of your app. See the client side routing guide to learn how to set this up. ## Sections# * * * Select supports sections in order to group options. Sections can be used by wrapping groups of items in a `ListBoxSection` element. A `<Header>` element may also be included to label the section. ### Static items# import {ListBoxSection, Header} from 'react-aria-components'; <MySelect label="Preferred fruit or vegetable"> <ListBoxSection> <Header>Fruit</Header> <ListBoxItem id="Apple">Apple</ListBoxItem> <ListBoxItem id="Banana">Banana</ListBoxItem> <ListBoxItem id="Orange">Orange</ListBoxItem> <ListBoxItem id="Honeydew">Honeydew</ListBoxItem> <ListBoxItem id="Grapes">Grapes</ListBoxItem> <ListBoxItem id="Watermelon">Watermelon</ListBoxItem> <ListBoxItem id="Cantaloupe">Cantaloupe</ListBoxItem> <ListBoxItem id="Pear">Pear</ListBoxItem> </ListBoxSection> <ListBoxSection> <Header>Vegetable</Header> <ListBoxItem id="Cabbage">Cabbage</ListBoxItem> <ListBoxItem id="Broccoli">Broccoli</ListBoxItem> <ListBoxItem id="Carrots">Carrots</ListBoxItem> <ListBoxItem id="Lettuce">Lettuce</ListBoxItem> <ListBoxItem id="Spinach">Spinach</ListBoxItem> <ListBoxItem id="Bok Choy">Bok Choy</ListBoxItem> <ListBoxItem id="Cauliflower">Cauliflower</ListBoxItem> <ListBoxItem id="Potatoes">Potatoes</ListBoxItem> </ListBoxSection> </MySelect> import { Header, ListBoxSection } from 'react-aria-components'; <MySelect label="Preferred fruit or vegetable"> <ListBoxSection> <Header>Fruit</Header> <ListBoxItem id="Apple">Apple</ListBoxItem> <ListBoxItem id="Banana">Banana</ListBoxItem> <ListBoxItem id="Orange">Orange</ListBoxItem> <ListBoxItem id="Honeydew">Honeydew</ListBoxItem> <ListBoxItem id="Grapes">Grapes</ListBoxItem> <ListBoxItem id="Watermelon">Watermelon</ListBoxItem> <ListBoxItem id="Cantaloupe">Cantaloupe</ListBoxItem> <ListBoxItem id="Pear">Pear</ListBoxItem> </ListBoxSection> <ListBoxSection> <Header>Vegetable</Header> <ListBoxItem id="Cabbage">Cabbage</ListBoxItem> <ListBoxItem id="Broccoli">Broccoli</ListBoxItem> <ListBoxItem id="Carrots">Carrots</ListBoxItem> <ListBoxItem id="Lettuce">Lettuce</ListBoxItem> <ListBoxItem id="Spinach">Spinach</ListBoxItem> <ListBoxItem id="Bok Choy">Bok Choy</ListBoxItem> <ListBoxItem id="Cauliflower"> Cauliflower </ListBoxItem> <ListBoxItem id="Potatoes">Potatoes</ListBoxItem> </ListBoxSection> </MySelect> import { Header, ListBoxSection } from 'react-aria-components'; <MySelect label="Preferred fruit or vegetable"> <ListBoxSection> <Header> Fruit </Header> <ListBoxItem id="Apple"> Apple </ListBoxItem> <ListBoxItem id="Banana"> Banana </ListBoxItem> <ListBoxItem id="Orange"> Orange </ListBoxItem> <ListBoxItem id="Honeydew"> Honeydew </ListBoxItem> <ListBoxItem id="Grapes"> Grapes </ListBoxItem> <ListBoxItem id="Watermelon"> Watermelon </ListBoxItem> <ListBoxItem id="Cantaloupe"> Cantaloupe </ListBoxItem> <ListBoxItem id="Pear"> Pear </ListBoxItem> </ListBoxSection> <ListBoxSection> <Header> Vegetable </Header> <ListBoxItem id="Cabbage"> Cabbage </ListBoxItem> <ListBoxItem id="Broccoli"> Broccoli </ListBoxItem> <ListBoxItem id="Carrots"> Carrots </ListBoxItem> <ListBoxItem id="Lettuce"> Lettuce </ListBoxItem> <ListBoxItem id="Spinach"> Spinach </ListBoxItem> <ListBoxItem id="Bok Choy"> Bok Choy </ListBoxItem> <ListBoxItem id="Cauliflower"> Cauliflower </ListBoxItem> <ListBoxItem id="Potatoes"> Potatoes </ListBoxItem> </ListBoxSection> </MySelect> Preferred fruit or vegetableSelect an item▼ AppleBananaOrangeHoneydewGrapesWatermelonCantaloupePearCabbageBroccoliCarrotsLettuceSpinachBok ChoyCauliflowerPotatoes ### Dynamic items# Sections used with dynamic items are populated from a hierarchical data structure. Similarly to the props on Select, `<ListBoxSection>` takes an array of data using the `items` prop. If the section also has a header, the `Collection` component can be used to render the child items. import {Collection} from 'react-aria-components'; function Example() { let options = [ {name: 'Fruit', children: [ {name: 'Apple'}, {name: 'Banana'}, {name: 'Orange'}, {name: 'Honeydew'}, {name: 'Grapes'}, {name: 'Watermelon'}, {name: 'Cantaloupe'}, {name: 'Pear'} ]}, {name: 'Vegetable', children: [ {name: 'Cabbage'}, {name: 'Broccoli'}, {name: 'Carrots'}, {name: 'Lettuce'}, {name: 'Spinach'}, {name: 'Bok Choy'}, {name: 'Cauliflower'}, {name: 'Potatoes'} ]} ]; return ( <MySelect label="Preferred fruit or vegetable" items={options}> {section => ( <ListBoxSection id={section.name}> <Header>{section.name}</Header> <Collection items={section.children}> {item => <ListBoxItem id={item.name}>{item.name}</ListBoxItem>} </Collection> </ListBoxSection> )} </MySelect> ); } import {Collection} from 'react-aria-components'; function Example() { let options = [ { name: 'Fruit', children: [ { name: 'Apple' }, { name: 'Banana' }, { name: 'Orange' }, { name: 'Honeydew' }, { name: 'Grapes' }, { name: 'Watermelon' }, { name: 'Cantaloupe' }, { name: 'Pear' } ] }, { name: 'Vegetable', children: [ { name: 'Cabbage' }, { name: 'Broccoli' }, { name: 'Carrots' }, { name: 'Lettuce' }, { name: 'Spinach' }, { name: 'Bok Choy' }, { name: 'Cauliflower' }, { name: 'Potatoes' } ] } ]; return ( <MySelect label="Preferred fruit or vegetable" items={options} > {(section) => ( <ListBoxSection id={section.name}> <Header>{section.name}</Header> <Collection items={section.children}> {(item) => ( <ListBoxItem id={item.name}> {item.name} </ListBoxItem> )} </Collection> </ListBoxSection> )} </MySelect> ); } import {Collection} from 'react-aria-components'; function Example() { let options = [ { name: 'Fruit', children: [ { name: 'Apple' }, { name: 'Banana' }, { name: 'Orange' }, { name: 'Honeydew' }, { name: 'Grapes' }, { name: 'Watermelon' }, { name: 'Cantaloupe' }, { name: 'Pear' } ] }, { name: 'Vegetable', children: [ { name: 'Cabbage' }, { name: 'Broccoli' }, { name: 'Carrots' }, { name: 'Lettuce' }, { name: 'Spinach' }, { name: 'Bok Choy' }, { name: 'Cauliflower' }, { name: 'Potatoes' } ] } ]; return ( <MySelect label="Preferred fruit or vegetable" items={options} > {(section) => ( <ListBoxSection id={section .name} > <Header> {section .name} </Header> <Collection items={section .children} > {(item) => ( <ListBoxItem id={item .name} > {item .name} </ListBoxItem> )} </Collection> </ListBoxSection> )} </MySelect> ); } Preferred fruit or vegetableSelect an item▼ AppleBananaOrangeHoneydewGrapesWatermelonCantaloupePearCabbageBroccoliCarrotsLettuceSpinachBok ChoyCauliflowerPotatoes ## Text slots# * * * By default, items in a Select are labeled by their text contents for accessibility. ListBoxItems also support the "label" and "description" slots to separate primary and secondary content, which improves screen reader announcements and can also be used for styling purposes. **Note**: The ARIA spec prohibits listbox items from including interactive content such as buttons, checkboxes, etc. import {Text} from 'react-aria-components'; <MySelect label="Permissions"> <ListBoxItem textValue="Read"> <Text slot="label">Read</Text> <Text slot="description">Read only</Text> </ListBoxItem> <ListBoxItem textValue="Write"> <Text slot="label">Write</Text> <Text slot="description">Read and write only</Text> </ListBoxItem> <ListBoxItem textValue="Admin"> <Text slot="label">Admin</Text> <Text slot="description">Full access</Text> </ListBoxItem> </MySelect> import {Text} from 'react-aria-components'; <MySelect label="Permissions"> <ListBoxItem textValue="Read"> <Text slot="label">Read</Text> <Text slot="description">Read only</Text> </ListBoxItem> <ListBoxItem textValue="Write"> <Text slot="label">Write</Text> <Text slot="description">Read and write only</Text> </ListBoxItem> <ListBoxItem textValue="Admin"> <Text slot="label">Admin</Text> <Text slot="description">Full access</Text> </ListBoxItem> </MySelect> import {Text} from 'react-aria-components'; <MySelect label="Permissions"> <ListBoxItem textValue="Read"> <Text slot="label"> Read </Text> <Text slot="description"> Read only </Text> </ListBoxItem> <ListBoxItem textValue="Write"> <Text slot="label"> Write </Text> <Text slot="description"> Read and write only </Text> </ListBoxItem> <ListBoxItem textValue="Admin"> <Text slot="label"> Admin </Text> <Text slot="description"> Full access </Text> </ListBoxItem> </MySelect> PermissionsSelect an item▼ ReadWriteAdmin Show CSS .react-aria-Select { .react-aria-SelectValue { [slot=description] { display: none; } } } .react-aria-Select { .react-aria-SelectValue { [slot=description] { display: none; } } } .react-aria-Select { .react-aria-SelectValue { [slot=description] { display: none; } } } ## Asynchronous loading# * * * This example uses the useAsyncList hook to handle asynchronous loading of data from a server. You may additionally want to display a spinner to indicate the loading state to the user. import {useAsyncList} from 'react-stately'; interface Character { name: string; } function AsyncLoadingExample() { let list = useAsyncList<Character>({ async load({ signal, filterText }) { let res = await fetch( `https://pokeapi.co/api/v2/pokemon`, { signal } ); let json = await res.json(); return { items: json.results }; } }); return ( <MySelect label="Pick a Pokemon" items={list.items}> {(item) => <ListBoxItem id={item.name}>{item.name}</ListBoxItem>} </MySelect> ); } import {useAsyncList} from 'react-stately'; interface Character { name: string; } function AsyncLoadingExample() { let list = useAsyncList<Character>({ async load({ signal, filterText }) { let res = await fetch( `https://pokeapi.co/api/v2/pokemon`, { signal } ); let json = await res.json(); return { items: json.results }; } }); return ( <MySelect label="Pick a Pokemon" items={list.items}> {(item) => ( <ListBoxItem id={item.name}> {item.name} </ListBoxItem> )} </MySelect> ); } import {useAsyncList} from 'react-stately'; interface Character { name: string; } function AsyncLoadingExample() { let list = useAsyncList< Character >({ async load( { signal, filterText } ) { let res = await fetch( `https://pokeapi.co/api/v2/pokemon`, { signal } ); let json = await res .json(); return { items: json.results }; } }); return ( <MySelect label="Pick a Pokemon" items={list.items} > {(item) => ( <ListBoxItem id={item.name} > {item.name} </ListBoxItem> )} </MySelect> ); } Pick a PokemonSelect an item▼ bulbasaurivysaurvenusaurcharmandercharmeleoncharizardsquirtlewartortleblastoisecaterpiemetapodbutterfreeweedlekakunabeedrillpidgeypidgeottopidgeotrattataraticate ## Disabled# * * * A Select can be fully disabled using the `isDisabled` prop. <MySelect label="Choose frequency" isDisabled> <ListBoxItem id="rarely">Rarely</ListBoxItem> <ListBoxItem id="sometimes">Sometimes</ListBoxItem> <ListBoxItem id="always">Always</ListBoxItem> </MySelect> <MySelect label="Choose frequency" isDisabled> <ListBoxItem id="rarely">Rarely</ListBoxItem> <ListBoxItem id="sometimes">Sometimes</ListBoxItem> <ListBoxItem id="always">Always</ListBoxItem> </MySelect> <MySelect label="Choose frequency" isDisabled > <ListBoxItem id="rarely"> Rarely </ListBoxItem> <ListBoxItem id="sometimes"> Sometimes </ListBoxItem> <ListBoxItem id="always"> Always </ListBoxItem> </MySelect> Choose frequencySelect an item▼ RarelySometimesAlways Show CSS .react-aria-Select { .react-aria-Button { &[data-disabled] { border-color: var(--border-color-disabled); color: var(--text-color-disabled); span[aria-hidden] { background: var(--border-color-disabled); color: var(--text-color-disabled); } .react-aria-SelectValue { &[data-placeholder] { color: var(--text-color-disabled); } } } } } @media (forced-colors: active) { .react-aria-Select { .react-aria-Button { &[data-disabled] span[aria-hidden] { background: 0 0; } } } } .react-aria-Select { .react-aria-Button { &[data-disabled] { border-color: var(--border-color-disabled); color: var(--text-color-disabled); span[aria-hidden] { background: var(--border-color-disabled); color: var(--text-color-disabled); } .react-aria-SelectValue { &[data-placeholder] { color: var(--text-color-disabled); } } } } } @media (forced-colors: active) { .react-aria-Select { .react-aria-Button { &[data-disabled] span[aria-hidden] { background: 0 0; } } } } .react-aria-Select { .react-aria-Button { &[data-disabled] { border-color: var(--border-color-disabled); color: var(--text-color-disabled); span[aria-hidden] { background: var(--border-color-disabled); color: var(--text-color-disabled); } .react-aria-SelectValue { &[data-placeholder] { color: var(--text-color-disabled); } } } } } @media (forced-colors: active) { .react-aria-Select { .react-aria-Button { &[data-disabled] span[aria-hidden] { background: 0 0; } } } } ### Disabled options# `Select` supports marking items as disabled using the `disabledKeys` prop. Each key in this list corresponds with the `id` prop passed to the `ListBoxItem` component, or automatically derived from the values passed to the `items` prop. See the Collections guide for more details. Disabled items are not focusable, selectable, or keyboard navigable. The `isDisabled` property returned by `useOption` can be used to style the item appropriately. <MySelect label="Favorite Animal" disabledKeys={['cat', 'kangaroo']}> <ListBoxItem id="red panda">Red Panda</ListBoxItem> <ListBoxItem id="cat">Cat</ListBoxItem> <ListBoxItem id="dog">Dog</ListBoxItem> <ListBoxItem id="aardvark">Aardvark</ListBoxItem> <ListBoxItem id="kangaroo">Kangaroo</ListBoxItem> <ListBoxItem id="snake">Snake</ListBoxItem> </MySelect> <MySelect label="Favorite Animal" disabledKeys={['cat', 'kangaroo']} > <ListBoxItem id="red panda">Red Panda</ListBoxItem> <ListBoxItem id="cat">Cat</ListBoxItem> <ListBoxItem id="dog">Dog</ListBoxItem> <ListBoxItem id="aardvark">Aardvark</ListBoxItem> <ListBoxItem id="kangaroo">Kangaroo</ListBoxItem> <ListBoxItem id="snake">Snake</ListBoxItem> </MySelect> <MySelect label="Favorite Animal" disabledKeys={[ 'cat', 'kangaroo' ]} > <ListBoxItem id="red panda"> Red Panda </ListBoxItem> <ListBoxItem id="cat"> Cat </ListBoxItem> <ListBoxItem id="dog"> Dog </ListBoxItem> <ListBoxItem id="aardvark"> Aardvark </ListBoxItem> <ListBoxItem id="kangaroo"> Kangaroo </ListBoxItem> <ListBoxItem id="snake"> Snake </ListBoxItem> </MySelect> Favorite AnimalSelect an item▼ Red PandaCatDogAardvarkKangarooSnake ## Validation# * * * Select supports the `isRequired` prop to ensure the user selects an option, as well as custom client and server-side validation. It can also be integrated with other form libraries. See the Forms guide to learn more. To display validation errors, add a `<FieldError>` element as a child of the Select. This allows you to render error messages from all of the above sources with consistent custom styles. import {Form, FieldError} from 'react-aria-components'; <Form> <Select name="animal" isRequired> <Label>Favorite Animal</Label> <Button> <SelectValue /> <span aria-hidden="true">▼</span> </Button> <FieldError /> <Popover> <ListBox> <ListBoxItem>Aardvark</ListBoxItem> <ListBoxItem>Cat</ListBoxItem> <ListBoxItem>Dog</ListBoxItem> <ListBoxItem>Kangaroo</ListBoxItem> <ListBoxItem>Panda</ListBoxItem> <ListBoxItem>Snake</ListBoxItem> </ListBox> </Popover> </Select> <Button type="submit">Submit</Button> </Form> import {Form, FieldError} from 'react-aria-components'; <Form> <Select name="animal" isRequired> <Label>Favorite Animal</Label> <Button> <SelectValue /> <span aria-hidden="true">▼</span> </Button> <FieldError /> <Popover> <ListBox> <ListBoxItem>Aardvark</ListBoxItem> <ListBoxItem>Cat</ListBoxItem> <ListBoxItem>Dog</ListBoxItem> <ListBoxItem>Kangaroo</ListBoxItem> <ListBoxItem>Panda</ListBoxItem> <ListBoxItem>Snake</ListBoxItem> </ListBox> </Popover> </Select> <Button type="submit">Submit</Button> </Form> import { FieldError, Form } from 'react-aria-components'; <Form> <Select name="animal" isRequired > <Label> Favorite Animal </Label> <Button> <SelectValue /> <span aria-hidden="true"> ▼ </span> </Button> <FieldError /> <Popover> <ListBox> <ListBoxItem> Aardvark </ListBoxItem> <ListBoxItem> Cat </ListBoxItem> <ListBoxItem> Dog </ListBoxItem> <ListBoxItem> Kangaroo </ListBoxItem> <ListBoxItem> Panda </ListBoxItem> <ListBoxItem> Snake </ListBoxItem> </ListBox> </Popover> </Select> <Button type="submit"> Submit </Button> </Form> Favorite AnimalSelect an item▼ AardvarkCatDogKangarooPandaSnake Submit Show CSS .react-aria-Select { .react-aria-FieldError { font-size: 12px; color: var(--invalid-color); } } .react-aria-Select { .react-aria-FieldError { font-size: 12px; color: var(--invalid-color); } } .react-aria-Select { .react-aria-FieldError { font-size: 12px; color: var(--invalid-color); } } By default, `FieldError` displays default validation messages provided by the browser. See Customizing error messages in the Forms guide to learn how to provide your own custom errors. ### Description# The `description` slot can be used to associate additional help text with a Select. <Select> <Label>Favorite Animal</Label> <Button> <SelectValue /> <span aria-hidden="true">▼</span> </Button> <Text slot="description">Please select an animal.</Text> <Popover> <ListBox> <ListBoxItem>Aardvark</ListBoxItem> <ListBoxItem>Cat</ListBoxItem> <ListBoxItem>Dog</ListBoxItem> <ListBoxItem>Kangaroo</ListBoxItem> <ListBoxItem>Panda</ListBoxItem> <ListBoxItem>Snake</ListBoxItem> </ListBox> </Popover> </Select> <Select> <Label>Favorite Animal</Label> <Button> <SelectValue /> <span aria-hidden="true">▼</span> </Button> <Text slot="description">Please select an animal.</Text> <Popover> <ListBox> <ListBoxItem>Aardvark</ListBoxItem> <ListBoxItem>Cat</ListBoxItem> <ListBoxItem>Dog</ListBoxItem> <ListBoxItem>Kangaroo</ListBoxItem> <ListBoxItem>Panda</ListBoxItem> <ListBoxItem>Snake</ListBoxItem> </ListBox> </Popover> </Select> <Select> <Label> Favorite Animal </Label> <Button> <SelectValue /> <span aria-hidden="true"> ▼ </span> </Button> <Text slot="description"> Please select an animal. </Text> <Popover> <ListBox> <ListBoxItem> Aardvark </ListBoxItem> <ListBoxItem> Cat </ListBoxItem> <ListBoxItem> Dog </ListBoxItem> <ListBoxItem> Kangaroo </ListBoxItem> <ListBoxItem> Panda </ListBoxItem> <ListBoxItem> Snake </ListBoxItem> </ListBox> </Popover> </Select> Favorite AnimalSelect an item▼Please select an animal. AardvarkCatDogKangarooPandaSnake Show CSS .react-aria-Select { [slot=description] { font-size: 12px; } } .react-aria-Select { [slot=description] { font-size: 12px; } } .react-aria-Select { [slot=description] { font-size: 12px; } } ## Controlled open state# * * * The open state of the select can be controlled via the `defaultOpen` and `isOpen` props function Example() { let [open, setOpen] = React.useState(false); return ( <> <p>Select is {open ? 'open' : 'closed'}</p> <MySelect label="Choose frequency" isOpen={open} onOpenChange={setOpen}> <ListBoxItem id="rarely">Rarely</ListBoxItem> <ListBoxItem id="sometimes">Sometimes</ListBoxItem> <ListBoxItem id="always">Always</ListBoxItem> </MySelect> </> ); } function Example() { let [open, setOpen] = React.useState(false); return ( <> <p>Select is {open ? 'open' : 'closed'}</p> <MySelect label="Choose frequency" isOpen={open} onOpenChange={setOpen} > <ListBoxItem id="rarely">Rarely</ListBoxItem> <ListBoxItem id="sometimes">Sometimes</ListBoxItem> <ListBoxItem id="always">Always</ListBoxItem> </MySelect> </> ); } function Example() { let [open, setOpen] = React.useState( false ); return ( <> <p> Select is {open ? 'open' : 'closed'} </p> <MySelect label="Choose frequency" isOpen={open} onOpenChange={setOpen} > <ListBoxItem id="rarely"> Rarely </ListBoxItem> <ListBoxItem id="sometimes"> Sometimes </ListBoxItem> <ListBoxItem id="always"> Always </ListBoxItem> </MySelect> </> ); } Select is closed Choose frequencySelect an item▼ RarelySometimesAlways ## Customizing SelectValue# * * * Select passes the rendered children of the selected item in the render props of the `SelectValue` as well as if the placeholder should be showing. You can use this to customize the value displayed in the Select. <Select> <Label>Favorite Animal</Label> <Button> <SelectValue> {({defaultChildren, isPlaceholder}) => { return isPlaceholder ? <><b>Animal</b> selection</> : defaultChildren; }} </SelectValue> <span aria-hidden="true">▼</span> </Button> <Popover> <ListBox> <ListBoxItem>Aardvark</ListBoxItem> <ListBoxItem>Cat</ListBoxItem> <ListBoxItem>Dog</ListBoxItem> <ListBoxItem>Kangaroo</ListBoxItem> <ListBoxItem>Panda</ListBoxItem> <ListBoxItem>Snake</ListBoxItem> </ListBox> </Popover> </Select> <Select> <Label>Favorite Animal</Label> <Button> <SelectValue> {({ defaultChildren, isPlaceholder }) => { return isPlaceholder ? ( <> <b>Animal</b> selection </> ) : defaultChildren; }} </SelectValue> <span aria-hidden="true">▼</span> </Button> <Popover> <ListBox> <ListBoxItem>Aardvark</ListBoxItem> <ListBoxItem>Cat</ListBoxItem> <ListBoxItem>Dog</ListBoxItem> <ListBoxItem>Kangaroo</ListBoxItem> <ListBoxItem>Panda</ListBoxItem> <ListBoxItem>Snake</ListBoxItem> </ListBox> </Popover> </Select> <Select> <Label> Favorite Animal </Label> <Button> <SelectValue> {( { defaultChildren, isPlaceholder } ) => { return isPlaceholder ? ( <> <b> Animal </b>{' '} selection </> ) : defaultChildren; }} </SelectValue> <span aria-hidden="true"> ▼ </span> </Button> <Popover> <ListBox> <ListBoxItem> Aardvark </ListBoxItem> <ListBoxItem> Cat </ListBoxItem> <ListBoxItem> Dog </ListBoxItem> <ListBoxItem> Kangaroo </ListBoxItem> <ListBoxItem> Panda </ListBoxItem> <ListBoxItem> Snake </ListBoxItem> </ListBox> </Popover> </Select> Favorite Animal**Animal** selection▼ AardvarkCatDogKangarooPandaSnake ## Props# * * * ### Select# | Name | Type | Default | Description | | --- | --- | --- | --- | | `placeholder` | `string` | `'Select an item' (localized)` | Temporary text that occupies the select when it is empty. | | `autoComplete` | `string` | — | Describes the type of autocomplete functionality the input should provide if any. See MDN. | | `name` | `string` | — | The name of the input, used when submitting an HTML form. | | `isOpen` | `boolean` | — | Sets the open state of the menu. | | `defaultOpen` | `boolean` | — | Sets the default open state of the menu. | | `disabledKeys` | `Iterable<Key>` | — | The item keys that are disabled. These items cannot be selected, focused, or otherwise interacted with. | | `isDisabled` | `boolean` | — | Whether the input is disabled. | | `isRequired` | `boolean` | — | Whether user input is required on the input before form submission. | | `isInvalid` | `boolean` | — | Whether the input value is invalid. | | `validate` | `( (value: Key )) => ValidationError | true | null | undefined` | — | A function that returns an error message if a given value is invalid. Validation errors are displayed to the user when the form is submitted if `validationBehavior="native"`. For realtime validation, use the `isInvalid` prop instead. | | `selectedKey` | `Key | null` | — | The currently selected key in the collection (controlled). | | `defaultSelectedKey` | `Key` | — | The initial selected key in the collection (uncontrolled). | | `autoFocus` | `boolean` | — | Whether the element should receive focus on render. | | `validationBehavior` | `'native' | 'aria'` | `'native'` | Whether to use native HTML form validation to prevent form submission when the value is missing or invalid, or mark the field as required or invalid via ARIA. | | `children` | `ReactNode | ( (values: SelectRenderProps & & { defaultChildren: ReactNode | | undefined } )) => ReactNode` | — | The children of the component. A function may be provided to alter the children based on component state. | | `className` | `string | ( (values: SelectRenderProps & & { defaultClassName: string | | undefined } )) => string` | — | The CSS className for the element. A function may be provided to compute the class based on component state. | | `style` | `CSSProperties | ( (values: SelectRenderProps & & { defaultStyle: CSSProperties } )) => CSSProperties | undefined` | — | The inline style for the element. A function may be provided to compute the style based on component state. | Events | Name | Type | Description | | --- | --- | --- | | `onOpenChange` | `( (isOpen: boolean )) => void` | Method that is called when the open state of the menu changes. | | `onSelectionChange` | `( (key: Key | | null )) => void` | Handler that is called when the selection changes. | | `onFocus` | `( (e: FocusEvent<Target> )) => void` | Handler that is called when the element receives focus. | | `onBlur` | `( (e: FocusEvent<Target> )) => void` | Handler that is called when the element loses focus. | | `onFocusChange` | `( (isFocused: boolean )) => void` | Handler that is called when the element's focus status changes. | | `onKeyDown` | `( (e: KeyboardEvent )) => void` | Handler that is called when a key is pressed. | | `onKeyUp` | `( (e: KeyboardEvent )) => void` | Handler that is called when a key is released. | Layout | Name | Type | Description | | --- | --- | --- | | `slot` | `string | null` | A slot name for the component. Slots allow the component to receive props from a parent component. An explicit `null` value indicates that the local props completely override all props received from a parent. | Accessibility | Name | Type | Description | | --- | --- | --- | | `id` | `string` | The element's unique identifier. See MDN. | | `excludeFromTabOrder` | `boolean` | Whether to exclude the element from the sequential tab order. If true, the element will not be focusable via the keyboard by tabbing. This should be avoided except in rare scenarios where an alternative means of accessing the element or its functionality via the keyboard is available. | | `aria-label` | `string` | Defines a string value that labels the current element. | | `aria-labelledby` | `string` | Identifies the element (or elements) that labels the current element. | | `aria-describedby` | `string` | Identifies the element (or elements) that describes the object. | | `aria-details` | `string` | Identifies the element (or elements) that provide a detailed, extended description for the object. | ### Label# A `<Label>` accepts all HTML attributes. ### Button# A `<Button>` accepts its contents as `children`. Other props such as `onPress` and `isDisabled` will be set by the `Select`. Show props | Name | Type | Default | Description | | --- | --- | --- | --- | | `form` | `string` | — | The `<form>` element to associate the button with. The value of this attribute must be the id of a `<form>` in the same document. | | `formAction` | `string` | — | The URL that processes the information submitted by the button. Overrides the action attribute of the button's form owner. | | `formEncType` | `string` | — | Indicates how to encode the form data that is submitted. | | `formMethod` | `string` | — | Indicates the HTTP method used to submit the form. | | `formNoValidate` | `boolean` | — | Indicates that the form is not to be validated when it is submitted. | | `formTarget` | `string` | — | Overrides the target attribute of the button's form owner. | | `name` | `string` | — | Submitted as a pair with the button's value as part of the form data. | | `value` | `string` | — | The value associated with the button's name when it's submitted with the form data. | | `isPending` | `boolean` | — | Whether the button is in a pending state. This disables press and hover events while retaining focusability, and announces the pending state to screen readers. | | `isDisabled` | `boolean` | — | Whether the button is disabled. | | `autoFocus` | `boolean` | — | Whether the element should receive focus on render. | | `type` | `'button' | 'submit' | 'reset'` | `'button'` | The behavior of the button when used in an HTML form. | | `children` | `ReactNode | ( (values: ButtonRenderProps & & { defaultChildren: ReactNode | | undefined } )) => ReactNode` | — | The children of the component. A function may be provided to alter the children based on component state. | | `className` | `string | ( (values: ButtonRenderProps & & { defaultClassName: string | | undefined } )) => string` | — | The CSS className for the element. A function may be provided to compute the class based on component state. | | `style` | `CSSProperties | ( (values: ButtonRenderProps & & { defaultStyle: CSSProperties } )) => CSSProperties | undefined` | — | The inline style for the element. A function may be provided to compute the style based on component state. | Events | Name | Type | Description | | --- | --- | --- | | `onPress` | `( (e: PressEvent )) => void` | Handler that is called when the press is released over the target. | | `onPressStart` | `( (e: PressEvent )) => void` | Handler that is called when a press interaction starts. | | `onPressEnd` | `( (e: PressEvent )) => void` | Handler that is called when a press interaction ends, either over the target or when the pointer leaves the target. | | `onPressChange` | `( (isPressed: boolean )) => void` | Handler that is called when the press state changes. | | `onPressUp` | `( (e: PressEvent )) => void` | Handler that is called when a press is released over the target, regardless of whether it started on the target or not. | | `onClick` | `( (e: MouseEvent<FocusableElement> )) => void` | **Not recommended – use `onPress` instead.** `onClick` is an alias for `onPress` provided for compatibility with other libraries. `onPress` provides additional event details for non-mouse interactions. | | `onFocus` | `( (e: FocusEvent<Target> )) => void` | Handler that is called when the element receives focus. | | `onBlur` | `( (e: FocusEvent<Target> )) => void` | Handler that is called when the element loses focus. | | `onFocusChange` | `( (isFocused: boolean )) => void` | Handler that is called when the element's focus status changes. | | `onKeyDown` | `( (e: KeyboardEvent )) => void` | Handler that is called when a key is pressed. | | `onKeyUp` | `( (e: KeyboardEvent )) => void` | Handler that is called when a key is released. | | `onHoverStart` | `( (e: HoverEvent )) => void` | Handler that is called when a hover interaction starts. | | `onHoverEnd` | `( (e: HoverEvent )) => void` | Handler that is called when a hover interaction ends. | | `onHoverChange` | `( (isHovering: boolean )) => void` | Handler that is called when the hover state changes. | Layout | Name | Type | Description | | --- | --- | --- | | `slot` | `string | null` | A slot name for the component. Slots allow the component to receive props from a parent component. An explicit `null` value indicates that the local props completely override all props received from a parent. | Accessibility | Name | Type | Description | | --- | --- | --- | | `id` | `string` | The element's unique identifier. See MDN. | | `excludeFromTabOrder` | `boolean` | Whether to exclude the element from the sequential tab order. If true, the element will not be focusable via the keyboard by tabbing. This should be avoided except in rare scenarios where an alternative means of accessing the element or its functionality via the keyboard is available. | | `preventFocusOnPress` | `boolean` | Whether to prevent focus from moving to the button when pressing it. Caution, this can make the button inaccessible and should only be used when alternative keyboard interaction is provided, such as ComboBox's MenuTrigger or a NumberField's increment/decrement control. | | `aria-expanded` | `boolean | 'true' | 'false'` | Indicates whether the element, or another grouping element it controls, is currently expanded or collapsed. | | `aria-haspopup` | `boolean | 'menu' | 'listbox' | 'tree' | 'grid' | 'dialog' | 'true' | 'false'` | Indicates the availability and type of interactive popup element, such as menu or dialog, that can be triggered by an element. | | `aria-controls` | `string` | Identifies the element (or elements) whose contents or presence are controlled by the current element. | | `aria-pressed` | `boolean | 'true' | 'false' | 'mixed'` | Indicates the current "pressed" state of toggle buttons. | | `aria-current` | `boolean | 'true' | 'false' | 'page' | 'step' | 'location' | 'date' | 'time'` | Indicates whether this element represents the current item within a container or set of related elements. | | `aria-label` | `string` | Defines a string value that labels the current element. | | `aria-labelledby` | `string` | Identifies the element (or elements) that labels the current element. | | `aria-describedby` | `string` | Identifies the element (or elements) that describes the object. | | `aria-details` | `string` | Identifies the element (or elements) that provide a detailed, extended description for the object. | ### SelectValue# The `<SelectValue>` component displays the current value of the select within the `<Button>`, or a placeholder if no value is selected. Show props | Name | Type | Description | | --- | --- | --- | | `children` | `ReactNode | ( (values: SelectValueRenderProps <object> & & { defaultChildren: ReactNode | | undefined } )) => ReactNode` | The children of the component. A function may be provided to alter the children based on component state. | | `className` | `string | ( (values: SelectValueRenderProps <object> & & { defaultClassName: string | | undefined } )) => string` | The CSS className for the element. A function may be provided to compute the class based on component state. | | `style` | `CSSProperties | ( (values: SelectValueRenderProps <object> & & { defaultStyle: CSSProperties } )) => CSSProperties | undefined` | The inline style for the element. A function may be provided to compute the style based on component state. | ### Popover# A `<Popover>` is a container to hold the `<ListBox>` suggestions for a Select. By default, it has a `placement` of `bottom start` within a `<Select>`, but this and other positioning properties may be customized. Show props | Name | Type | Default | Description | | --- | --- | --- | --- | | `trigger` | `string` | — | The name of the component that triggered the popover. This is reflected on the element as the `data-trigger` attribute, and can be used to provide specific styles for the popover depending on which element triggered it. | | `triggerRef` | ` RefObject <Element | null>` | — | The ref for the element which the popover positions itself with respect to. When used within a trigger component such as DialogTrigger, MenuTrigger, Select, etc., this is set automatically. It is only required when used standalone. | | `isEntering` | `boolean` | — | Whether the popover is currently performing an entry animation. | | `isExiting` | `boolean` | — | Whether the popover is currently performing an exit animation. | | `offset` | `number` | `8` | The additional offset applied along the main axis between the element and its anchor element. | | `placement` | ` Placement ` | `'bottom'` | The placement of the element with respect to its anchor element. | | `containerPadding` | `number` | `12` | The placement padding that should be applied between the element and its surrounding container. | | `crossOffset` | `number` | `0` | The additional offset applied along the cross axis between the element and its anchor element. | | `shouldFlip` | `boolean` | `true` | Whether the element should flip its orientation (e.g. top to bottom or left to right) when there is insufficient room for it to render completely. | | `isNonModal` | `boolean` | — | Whether the popover is non-modal, i.e. elements outside the popover may be interacted with by assistive technologies. Most popovers should not use this option as it may negatively impact the screen reader experience. Only use with components such as combobox, which are designed to handle this situation carefully. | | `isKeyboardDismissDisabled` | `boolean` | `false` | Whether pressing the escape key to close the popover should be disabled. Most popovers should not use this option. When set to true, an alternative way to close the popover with a keyboard must be provided. | | `shouldCloseOnInteractOutside` | `( (element: Element )) => boolean` | — | When user interacts with the argument element outside of the popover ref, return true if onClose should be called. This gives you a chance to filter out interaction with elements that should not dismiss the popover. By default, onClose will always be called on interaction outside the popover ref. | | `boundaryElement` | `Element` | `document.body` | Element that that serves as the positioning boundary. | | `scrollRef` | ` RefObject <Element | null>` | `overlayRef` | A ref for the scrollable region within the overlay. | | `shouldUpdatePosition` | `boolean` | `true` | Whether the overlay should update its position automatically. | | `arrowBoundaryOffset` | `number` | `0` | The minimum distance the arrow's edge should be from the edge of the overlay element. | | `isOpen` | `boolean` | — | Whether the overlay is open by default (controlled). | | `defaultOpen` | `boolean` | — | Whether the overlay is open by default (uncontrolled). | | `children` | `ReactNode | ( (values: PopoverRenderProps & & { defaultChildren: ReactNode | | undefined } )) => ReactNode` | — | The children of the component. A function may be provided to alter the children based on component state. | | `className` | `string | ( (values: PopoverRenderProps & & { defaultClassName: string | | undefined } )) => string` | — | The CSS className for the element. A function may be provided to compute the class based on component state. | | `style` | `CSSProperties | ( (values: PopoverRenderProps & & { defaultStyle: CSSProperties } )) => CSSProperties | undefined` | — | The inline style for the element. A function may be provided to compute the style based on component state. | Events | Name | Type | Description | | --- | --- | --- | | `onOpenChange` | `( (isOpen: boolean )) => void` | Handler that is called when the overlay's open state changes. | Layout | Name | Type | Description | | --- | --- | --- | | `slot` | `string | null` | A slot name for the component. Slots allow the component to receive props from a parent component. An explicit `null` value indicates that the local props completely override all props received from a parent. | Sizing | Name | Type | Description | | --- | --- | --- | | `maxHeight` | `number` | The maxHeight specified for the overlay element. By default, it will take all space up to the current viewport height. | Accessibility | Name | Type | Description | | --- | --- | --- | | `aria-label` | `string` | Defines a string value that labels the current element. | | `aria-labelledby` | `string` | Identifies the element (or elements) that labels the current element. | | `aria-describedby` | `string` | Identifies the element (or elements) that describes the object. | | `aria-details` | `string` | Identifies the element (or elements) that provide a detailed, extended description for the object. | ### ListBox# Within a `<Select>`, most `<ListBox>` props are set automatically. The `<ListBox>` defines the options to display in a Select. Show props | Name | Type | Default | Description | | --- | --- | --- | --- | | `selectionBehavior` | ` SelectionBehavior ` | — | How multiple selection should behave in the collection. | | `dragAndDropHooks` | ` DragAndDropHooks ` | — | The drag and drop hooks returned by `useDragAndDrop` used to enable drag and drop behavior for the ListBox. | | `renderEmptyState` | `( (props: ListBoxRenderProps )) => ReactNode` | — | Provides content to display when there are no items in the list. | | `layout` | `'stack' | 'grid'` | `'stack'` | Whether the items are arranged in a stack or grid. | | `orientation` | ` Orientation ` | `'vertical'` | The primary orientation of the items. Usually this is the direction that the collection scrolls. | | `shouldSelectOnPressUp` | `boolean` | — | Whether selection should occur on press up instead of press down. | | `shouldFocusOnHover` | `boolean` | — | Whether options should be focused when the user hovers over them. | | `escapeKeyBehavior` | `'clearSelection' | 'none'` | `'clearSelection'` | Whether pressing the escape key should clear selection in the listbox or not. Most experiences should not modify this option as it eliminates a keyboard user's ability to easily clear selection. Only use if the escape key is being handled externally or should not trigger selection clearing contextually. | | `autoFocus` | `boolean | FocusStrategy ` | — | Whether to auto focus the listbox or an option. | | `shouldFocusWrap` | `boolean` | — | Whether focus should wrap around when the end/start is reached. | | `items` | `Iterable<T>` | — | Item objects in the collection. | | `disabledKeys` | `Iterable<Key>` | — | The item keys that are disabled. These items cannot be selected, focused, or otherwise interacted with. | | `selectionMode` | ` SelectionMode ` | — | The type of selection that is allowed in the collection. | | `disallowEmptySelection` | `boolean` | — | Whether the collection allows empty selection. | | `selectedKeys` | `'all' | Iterable<Key>` | — | The currently selected keys in the collection (controlled). | | `defaultSelectedKeys` | `'all' | Iterable<Key>` | — | The initial selected keys in the collection (uncontrolled). | | `children` | `ReactNode | ( (item: object )) => ReactNode` | — | The contents of the collection. | | `dependencies` | `ReadonlyArray<any>` | — | Values that should invalidate the item cache when using dynamic collections. | | `className` | `string | ( (values: ListBoxRenderProps & & { defaultClassName: string | | undefined } )) => string` | — | The CSS className for the element. A function may be provided to compute the class based on component state. | | `style` | `CSSProperties | ( (values: ListBoxRenderProps & & { defaultStyle: CSSProperties } )) => CSSProperties | undefined` | — | The inline style for the element. A function may be provided to compute the style based on component state. | Events | Name | Type | Description | | --- | --- | --- | | `onAction` | `( (key: Key )) => void` | Handler that is called when a user performs an action on an item. The exact user event depends on the collection's `selectionBehavior` prop and the interaction modality. | | `onSelectionChange` | `( (keys: Selection )) => void` | Handler that is called when the selection changes. | | `onFocus` | `( (e: FocusEvent<Target> )) => void` | Handler that is called when the element receives focus. | | `onBlur` | `( (e: FocusEvent<Target> )) => void` | Handler that is called when the element loses focus. | | `onFocusChange` | `( (isFocused: boolean )) => void` | Handler that is called when the element's focus status changes. | | `onScroll` | `( (e: UIEvent<Element> )) => void` | Handler that is called when a user scrolls. See MDN. | Layout | Name | Type | Description | | --- | --- | --- | | `slot` | `string | null` | A slot name for the component. Slots allow the component to receive props from a parent component. An explicit `null` value indicates that the local props completely override all props received from a parent. | Accessibility | Name | Type | Description | | --- | --- | --- | | `id` | `string` | The element's unique identifier. See MDN. | | `aria-label` | `string` | Defines a string value that labels the current element. | | `aria-labelledby` | `string` | Identifies the element (or elements) that labels the current element. | | `aria-describedby` | `string` | Identifies the element (or elements) that describes the object. | | `aria-details` | `string` | Identifies the element (or elements) that provide a detailed, extended description for the object. | ### ListBoxSection# A `<ListBoxSection>` defines the child items for a section within a `<ListBox>`. It may also contain an optional `<Header>` element. If there is no header, then an `aria-label` must be provided to identify the section to assistive technologies. Show props | Name | Type | Description | | --- | --- | --- | | `id` | `Key` | The unique id of the section. | | `value` | `object` | The object value that this section represents. When using dynamic collections, this is set automatically. | | `children` | `ReactNode | ( (item: object )) => ReactElement` | Static child items or a function to render children. | | `dependencies` | `ReadonlyArray<any>` | Values that should invalidate the item cache when using dynamic collections. | | `items` | `Iterable<object>` | Item objects in the section. | | `className` | `string` | The CSS className for the element. | | `style` | `CSSProperties` | The inline style for the element. | Accessibility | Name | Type | Description | | --- | --- | --- | | `aria-label` | `string` | An accessibility label for the section. | ### Header# A `<Header>` defines the title for a `<ListBoxSection>`. It accepts all DOM attributes. ### ListBoxItem# A `<ListBoxItem>` defines a single option within a `<ListBox>`. If the `children` are not plain text, then the `textValue` prop must also be set to a plain text representation, which will be used for autocomplete in the Select. Show props | Name | Type | Description | | --- | --- | --- | | `id` | `Key` | The unique id of the item. | | `value` | `object` | The object value that this item represents. When using dynamic collections, this is set automatically. | | `textValue` | `string` | A string representation of the item's contents, used for features like typeahead. | | `isDisabled` | `boolean` | Whether the item is disabled. | | `children` | `ReactNode | ( (values: ListBoxItemRenderProps & & { defaultChildren: ReactNode | | undefined } )) => ReactNode` | The children of the component. A function may be provided to alter the children based on component state. | | `className` | `string | ( (values: ListBoxItemRenderProps & & { defaultClassName: string | | undefined } )) => string` | The CSS className for the element. A function may be provided to compute the class based on component state. | | `style` | `CSSProperties | ( (values: ListBoxItemRenderProps & & { defaultStyle: CSSProperties } )) => CSSProperties | undefined` | The inline style for the element. A function may be provided to compute the style based on component state. | | `href` | ` Href ` | A URL to link to. See MDN. | | `hrefLang` | `string` | Hints at the human language of the linked URL. SeeMDN. | | `target` | `HTMLAttributeAnchorTarget` | The target window for the link. See MDN. | | `rel` | `string` | The relationship between the linked resource and the current page. See MDN. | | `download` | `boolean | string` | Causes the browser to download the linked URL. A string may be provided to suggest a file name. See MDN. | | `ping` | `string` | A space-separated list of URLs to ping when the link is followed. See MDN. | | `referrerPolicy` | `HTMLAttributeReferrerPolicy` | How much of the referrer to send when following the link. See MDN. | | `routerOptions` | ` RouterOptions ` | Options for the configured client side router. | Events | Name | Type | Description | | --- | --- | --- | | `onAction` | `() => void` | Handler that is called when a user performs an action on the item. The exact user event depends on the collection's `selectionBehavior` prop and the interaction modality. | | `onHoverStart` | `( (e: HoverEvent )) => void` | Handler that is called when a hover interaction starts. | | `onHoverEnd` | `( (e: HoverEvent )) => void` | Handler that is called when a hover interaction ends. | | `onHoverChange` | `( (isHovering: boolean )) => void` | Handler that is called when the hover state changes. | Accessibility | Name | Type | Description | | --- | --- | --- | | `aria-label` | `string` | An accessibility label for this item. | ### FieldError# A `<FieldError>` displays validation errors. Show props | Name | Type | Description | | --- | --- | --- | | `children` | `ReactNode | ( (values: FieldErrorRenderProps & & { defaultChildren: ReactNode | | undefined } )) => ReactNode` | The children of the component. A function may be provided to alter the children based on component state. | | `className` | `string | ( (values: FieldErrorRenderProps & & { defaultClassName: string | | undefined } )) => string` | The CSS className for the element. A function may be provided to compute the class based on component state. | | `style` | `CSSProperties | ( (values: FieldErrorRenderProps & & { defaultStyle: CSSProperties } )) => CSSProperties | undefined` | The inline style for the element. A function may be provided to compute the style based on component state. | Accessibility | Name | Type | Description | | --- | --- | --- | | `id` | `string` | The element's unique identifier. See MDN. | ## Styling# * * * React Aria components can be styled in many ways, including using CSS classes, inline styles, utility classes (e.g. Tailwind), CSS-in-JS (e.g. Styled Components), etc. By default, all components include a builtin `className` attribute which can be targeted using CSS selectors. These follow the `react-aria-ComponentName` naming convention. .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> In addition, some components support multiple UI states (e.g. pressed, hovered, etc.). React Aria components expose states using data attributes, which you can target in CSS selectors. 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] { /* ... */ } The `className` and `style` props also accept functions which receive states for styling. This lets you dynamically determine the classes or styles to apply, which is useful when using utility CSS libraries like Tailwind. <ListBoxItem className={({ isSelected }) => isSelected ? 'bg-blue-400' : 'bg-gray-100'} > Item </ListBoxItem> <ListBoxItem className={({ isSelected }) => isSelected ? 'bg-blue-400' : 'bg-gray-100'} > Item </ListBoxItem> <ListBoxItem className={( { isSelected } ) => isSelected ? 'bg-blue-400' : 'bg-gray-100'} > 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 />} Item </> )} </ListBoxItem> <ListBoxItem> {({isSelected}) => ( <> {isSelected && <CheckmarkIcon />} Item </> )} </ListBoxItem> <ListBoxItem> {( { isSelected } ) => ( <> {isSelected && ( <CheckmarkIcon /> )} Item </> )} </ListBoxItem> The states and selectors for each component used in a `Select` are documented below. ### Select# A `Select` can be targeted with the `.react-aria-Select` CSS selector, or by overriding with a custom `className`. It supports the following states: | Name | CSS Selector | Description | | --- | --- | --- | | `isFocused` | `[data-focused]` | Whether the select is focused, either via a mouse or keyboard. | | `isFocusVisible` | `[data-focus-visible]` | Whether the select is keyboard focused. | | `isDisabled` | `[data-disabled]` | Whether the select is disabled. | | `isOpen` | `[data-open]` | Whether the select is currently open. | | `isInvalid` | `[data-invalid]` | Whether the select is invalid. | | `isRequired` | `[data-required]` | Whether the select is required. | ### Label# A `Label` can be targeted with the `.react-aria-Label` CSS selector, or by overriding with a custom `className`. ### Button# A Button can be targeted with the `.react-aria-Button` CSS selector, or by overriding with a custom `className`. It supports the following states: | Name | CSS Selector | Description | | --- | --- | --- | | `isHovered` | `[data-hovered]` | Whether the button is currently hovered with a mouse. | | `isPressed` | `[data-pressed]` | Whether the button is currently in a pressed state. | | `isFocused` | `[data-focused]` | Whether the button is focused, either via a mouse or keyboard. | | `isFocusVisible` | `[data-focus-visible]` | Whether the button is keyboard focused. | | `isDisabled` | `[data-disabled]` | Whether the button is disabled. | | `isPending` | `[data-pending]` | Whether the button is currently in a pending state. | ### SelectValue# A `SelectValue` can be targeted with the `.react-aria-SelectValue` CSS selector, or by overriding with a custom `className`. It supports the following states and render props: | Name | CSS Selector | Description | | --- | --- | --- | | `isPlaceholder` | `[data-placeholder]` | Whether the value is a placeholder. | | `selectedItem` | `—` | The object value of the currently selected item. | | `selectedText` | `—` | The textValue of the currently selected item. | ### Popover# The Popover component can be targeted with the `.react-aria-Popover` CSS selector, or by overriding with a custom `className`. Note that it renders in a React Portal, so it will not appear as a descendant of the Select in the DOM. It supports the following states and render props: | Name | CSS Selector | Description | | --- | --- | --- | | `trigger` | `[data-trigger="..."]` | The name of the component that triggered the popover, e.g. "DialogTrigger" or "ComboBox". | | `placement` | `[data-placement="left | right | top | bottom"]` | The placement of the popover relative to the trigger. | | `isEntering` | `[data-entering]` | Whether the popover is currently entering. Use this to apply animations. | | `isExiting` | `[data-exiting]` | Whether the popover is currently exiting. Use this to apply animations. | Within a Select, the popover will have the `data-trigger="Select"` attribute, which can be used to define select-specific styles. In addition, the `--trigger-width` CSS custom property will be set on the popover, which you can use to make the popover match the width of the select button. .react-aria-Popover[data-trigger=Select] { width: var(--trigger-width); } .react-aria-Popover[data-trigger=Select] { width: var(--trigger-width); } .react-aria-Popover[data-trigger=Select] { width: var(--trigger-width); } ### ListBox# A `ListBox` can be targeted with the `.react-aria-ListBox` CSS selector, or by overriding with a custom `className`. ### ListBoxSection# A `ListBoxSection` can be targeted with the `.react-aria-ListBoxSection` CSS selector, or by overriding with a custom `className`. See sections for examples. ### Header# A `Header` within a `ListBoxSection` can be targeted with the `.react-aria-Header` CSS selector, or by overriding with a custom `className`. See sections for examples. ### ListBoxItem# A `ListBoxItem` can be targeted with the `.react-aria-ListBoxItem` CSS selector, or by overriding with a custom `className`. It supports the following states and render props: | Name | CSS Selector | Description | | --- | --- | --- | | `isHovered` | `[data-hovered]` | Whether the item is currently hovered with a mouse. | | `isPressed` | `[data-pressed]` | Whether the item is currently in a pressed state. | | `isSelected` | `[data-selected]` | Whether the item is currently selected. | | `isFocused` | `[data-focused]` | Whether the item is currently focused. | | `isFocusVisible` | `[data-focus-visible]` | Whether the item is currently keyboard focused. | | `isDisabled` | `[data-disabled]` | Whether the item is non-interactive, i.e. both selection and actions are disabled and the item may not be focused. Dependent on `disabledKeys` and `disabledBehavior`. | | `selectionMode` | `[data-selection-mode="single | multiple"]` | The type of selection that is allowed in the collection. | | `selectionBehavior` | `—` | The selection behavior for the collection. | Items also support two slots: a label, and a description. When provided using the `<Text>` element, the item will have `aria-labelledby` and `aria-describedby` attributes pointing to these slots, improving screen reader announcement. See text slots for an example. Note that items may not contain interactive children such as buttons, as screen readers will not be able to access them. ### Text# The help text elements within a `Select` can be targeted with the `[slot=description]` and `[slot=errorMessage]` CSS selectors, or by adding a custom `className`. ### FieldError# A `FieldError` can be targeted with the `.react-aria-FieldError` CSS selector, or by overriding with a custom `className`. It supports the following render props: | Name | Description | | --- | --- | | `isInvalid` | Whether the input value is invalid. | | `validationErrors` | The current error messages for the input if it is invalid, otherwise an empty array. | | `validationDetails` | The native validation details for the input. | ## Advanced customization# * * * ### Composition# If you need to customize one of the components within a `Select`, such as `Button` or `ListBox`, in many cases you can create a wrapper component. This lets you customize the props passed to the component. function MyListBox(props) { return <ListBox {...props} className="my-listbox" /> } function MyListBox(props) { return <ListBox {...props} className="my-listbox" /> } function MyListBox( props ) { return ( <ListBox {...props} className="my-listbox" /> ); } ### Contexts# All React Aria Components export a corresponding context that can be used to send props to them from a parent element. This enables you to build your own compositional APIs similar to those found in React Aria Components itself. You can send any prop or ref via context that you could pass to the corresponding component. The local props and ref on the component are merged with the ones passed via context, with the local props taking precedence (following the rules documented in mergeProps). | Component | Context | Props | Ref | | --- | --- | --- | --- | | `Select` | `SelectContext` | ` SelectProps ` | `HTMLDivElement` | This example shows a `FieldGroup` component that renders a group of selects with a title. The entire group can be marked as disabled via the `isDisabled` prop, which is passed to all child selects via the `SelectContext` provider. import {SelectContext} from 'react-aria-components'; interface FieldGroupProps { title?: string, children?: React.ReactNode, isDisabled?: boolean } function FieldGroup({title, children, isDisabled}: FieldGroupProps) { return ( <fieldset> <legend>{title}</legend> <SelectContext.Provider value={{isDisabled}}> {children} </SelectContext.Provider> </fieldset> ); } <FieldGroup title="Filters" isDisabled> <MySelect label="Status" defaultSelectedKey="published"> <ListBoxItem id="draft">Draft</ListBoxItem> <ListBoxItem id="published">Published</ListBoxItem> <ListBoxItem id="deleted">Deleted</ListBoxItem> </MySelect> <MySelect label="Author" defaultSelectedKey="emma"> <ListBoxItem id="john">John</ListBoxItem> <ListBoxItem id="emma">Emma</ListBoxItem> <ListBoxItem id="tim">Tim</ListBoxItem> </MySelect> </FieldGroup> import {SelectContext} from 'react-aria-components'; interface FieldGroupProps { title?: string; children?: React.ReactNode; isDisabled?: boolean; } function FieldGroup( { title, children, isDisabled }: FieldGroupProps ) { return ( <fieldset> <legend>{title}</legend> <SelectContext.Provider value={{ isDisabled }}> {children} </SelectContext.Provider> </fieldset> ); } <FieldGroup title="Filters" isDisabled> <MySelect label="Status" defaultSelectedKey="published"> <ListBoxItem id="draft">Draft</ListBoxItem> <ListBoxItem id="published">Published</ListBoxItem> <ListBoxItem id="deleted">Deleted</ListBoxItem> </MySelect> <MySelect label="Author" defaultSelectedKey="emma"> <ListBoxItem id="john">John</ListBoxItem> <ListBoxItem id="emma">Emma</ListBoxItem> <ListBoxItem id="tim">Tim</ListBoxItem> </MySelect> </FieldGroup> import {SelectContext} from 'react-aria-components'; interface FieldGroupProps { title?: string; children?: React.ReactNode; isDisabled?: boolean; } function FieldGroup( { title, children, isDisabled }: FieldGroupProps ) { return ( <fieldset> <legend> {title} </legend> <SelectContext.Provider value={{ isDisabled }} > {children} </SelectContext.Provider> </fieldset> ); } <FieldGroup title="Filters" isDisabled > <MySelect label="Status" defaultSelectedKey="published" > <ListBoxItem id="draft"> Draft </ListBoxItem> <ListBoxItem id="published"> Published </ListBoxItem> <ListBoxItem id="deleted"> Deleted </ListBoxItem> </MySelect> <MySelect label="Author" defaultSelectedKey="emma" > <ListBoxItem id="john"> John </ListBoxItem> <ListBoxItem id="emma"> Emma </ListBoxItem> <ListBoxItem id="tim"> Tim </ListBoxItem> </MySelect> </FieldGroup> Filters StatusPublished▼ DraftPublishedDeleted AuthorEmma▼ JohnEmmaTim Show CSS fieldset { padding: 1.5em; width: fit-content; } fieldset { padding: 1.5em; width: fit-content; } fieldset { padding: 1.5em; width: fit-content; } ### Custom children# Select passes props to its child components, such as the label and popover, via their associated contexts. These contexts are exported so you can also consume them in your own custom components. This enables you to reuse existing components from your app or component library together with React Aria Components. | Component | Context | Props | Ref | | --- | --- | --- | --- | | `Label` | `LabelContext` | ` LabelProps ` | `HTMLLabelElement` | | `Button` | `ButtonContext` | ` ButtonProps ` | `HTMLButtonElement` | | `Popover` | `PopoverContext` | ` PopoverProps ` | `HTMLElement` | | `ListBox` | `ListBoxContext` | ` ListBoxProps ` | `HTMLDivElement` | | `Text` | `TextContext` | ` TextProps ` | `HTMLElement` | This example consumes from `LabelContext` in an existing styled label component to make it compatible with React Aria Components. The `useContextProps` hook merges the local props and ref with the ones provided via context by Select. import type {LabelProps} from 'react-aria-components'; import {LabelContext, useContextProps} from 'react-aria-components'; const MyCustomLabel = React.forwardRef( (props: LabelProps, ref: React.ForwardedRef<HTMLLabelElement>) => { // Merge the local props and ref with the ones provided via context. [props, ref] = useContextProps(props, ref, LabelContext); // ... your existing Label component return <label {...props} ref={ref} />; } ); import type {LabelProps} from 'react-aria-components'; import { LabelContext, useContextProps } from 'react-aria-components'; const MyCustomLabel = React.forwardRef( ( props: LabelProps, ref: React.ForwardedRef<HTMLLabelElement> ) => { // Merge the local props and ref with the ones provided via context. [props, ref] = useContextProps( props, ref, LabelContext ); // ... your existing Label component return <label {...props} ref={ref} />; } ); import type {LabelProps} from 'react-aria-components'; import { LabelContext, useContextProps } from 'react-aria-components'; const MyCustomLabel = React.forwardRef( ( props: LabelProps, ref: React.ForwardedRef< HTMLLabelElement > ) => { // Merge the local props and ref with the ones provided via context. [props, ref] = useContextProps( props, ref, LabelContext ); // ... your existing Label component return ( <label {...props} ref={ref} /> ); } ); Now you can use `MyCustomLabel` within a `Select`, in place of the builtin React Aria Components `Label`. <Select> <MyCustomLabel>Name</MyCustomLabel> {/* ... */} </Select> <Select> <MyCustomLabel>Name</MyCustomLabel> {/* ... */} </Select> <Select> <MyCustomLabel> Name </MyCustomLabel> {/* ... */} </Select> ### State# Select provides an `SelectState` object to its children via `SelectStateContext`. This can be used to access and manipulate the select's state. This example shows a `SelectClearButton` component that can be placed within a `Select` to allow the user to clear the selected item. import {SelectStateContext} from 'react-aria-components'; function SelectClearButton() { let state = React.useContext(SelectStateContext); return ( <Button // Don't inherit behavior from Select. slot={null} style={{fontSize: 'small', marginTop: 6, padding: 4}} onPress={() => state?.setSelectedKey(null)}> Clear </Button> ); } <Select> <Label>Favorite Animal</Label> <Button> <SelectValue /> <span aria-hidden="true">▼</span> </Button> <SelectClearButton /> <Popover> <ListBox> <ListBoxItem>Cat</ListBoxItem> <ListBoxItem>Dog</ListBoxItem> <ListBoxItem>Kangaroo</ListBoxItem> </ListBox> </Popover> </Select> import {SelectStateContext} from 'react-aria-components'; function SelectClearButton() { let state = React.useContext(SelectStateContext); return ( <Button // Don't inherit behavior from Select. slot={null} style={{fontSize: 'small', marginTop: 6, padding: 4}} onPress={() => state?.setSelectedKey(null)}> Clear </Button> ); } <Select> <Label>Favorite Animal</Label> <Button> <SelectValue /> <span aria-hidden="true">▼</span> </Button> <SelectClearButton /> <Popover> <ListBox> <ListBoxItem>Cat</ListBoxItem> <ListBoxItem>Dog</ListBoxItem> <ListBoxItem>Kangaroo</ListBoxItem> </ListBox> </Popover> </Select> import {SelectStateContext} from 'react-aria-components'; function SelectClearButton() { let state = React .useContext( SelectStateContext ); return ( <Button // Don't inherit behavior from Select. slot={null} style={{ fontSize: 'small', marginTop: 6, padding: 4 }} onPress={() => state ?.setSelectedKey( null )} > Clear </Button> ); } <Select> <Label> Favorite Animal </Label> <Button> <SelectValue /> <span aria-hidden="true"> ▼ </span> </Button> <SelectClearButton /> <Popover> <ListBox> <ListBoxItem> Cat </ListBoxItem> <ListBoxItem> Dog </ListBoxItem> <ListBoxItem> Kangaroo </ListBoxItem> </ListBox> </Popover> </Select> Favorite AnimalSelect an item▼Clear CatDogKangaroo ### Hooks# If you need to customize things even further, such as accessing internal state, intercepting events, or customizing the DOM structure, you can drop down to the lower level Hook-based API. See useSelect for more details. ## Accessibility# * * * ### False positives# Select may trigger a known accessibility false positive from automated accessibility testing tools. This is because we include a visually hidden select element alongside the Select to specifically aid with browser form autocomplete and hide it from screen readers via `aria-hidden` since users don't need to interact with the hidden select. We manage focus internally so that focusing this hidden select will always shift focus to the Select's trigger button instead. Automated accessibility testing tools have no way of knowing that we manage the focus in this way and thus throw this false positive. To facilitate the suppression of this false positive, the `data-a11y-ignore="aria-hidden-focus"` data attribute is automatically applied to the problematic element and references the relevant `AXE` rule. Please use this data attribute to target the problematic element and exclude it from your automated accessibility tests as shown here. ## Testing# * * * ### Test utils alpha# `@react-aria/test-utils` offers common select interaction utilities which you may find helpful when writing tests. See here for more information on how to setup these utilities in your tests. Below is the full definition of the select tester and a sample of how you could use it in your test suite. // Select.test.ts import {render} from '@testing-library/react'; import {User} from '@react-aria/test-utils'; let testUtilUser = new User({ interactionType: 'mouse' }); // ... it('Select can select an option via keyboard', async function () { // Render your test component/app and initialize the select tester let { getByTestId } = render( <Select data-testid="test-select"> ... </Select> ); let selectTester = testUtilUser.createTester('Select', { root: getByTestId('test-select'), interactionType: 'keyboard' }); let trigger = selectTester.trigger; expect(trigger).toHaveTextContent('Select an item'); await selectTester.selectOption({ option: 'Cat' }); expect(trigger).toHaveTextContent('Cat'); }); // Select.test.ts import {render} from '@testing-library/react'; import {User} from '@react-aria/test-utils'; let testUtilUser = new User({ interactionType: 'mouse' }); // ... it('Select can select an option via keyboard', async function () { // Render your test component/app and initialize the select tester let { getByTestId } = render( <Select data-testid="test-select"> ... </Select> ); let selectTester = testUtilUser.createTester('Select', { root: getByTestId('test-select'), interactionType: 'keyboard' }); let trigger = selectTester.trigger; expect(trigger).toHaveTextContent('Select an item'); await selectTester.selectOption({ option: 'Cat' }); expect(trigger).toHaveTextContent('Cat'); }); // Select.test.ts import {render} from '@testing-library/react'; import {User} from '@react-aria/test-utils'; let testUtilUser = new User({ interactionType: 'mouse' }); // ... it('Select can select an option via keyboard', async function () { // Render your test component/app and initialize the select tester let { getByTestId } = render( <Select data-testid="test-select"> ... </Select> ); let selectTester = testUtilUser .createTester( 'Select', { root: getByTestId( 'test-select' ), interactionType: 'keyboard' } ); let trigger = selectTester.trigger; expect(trigger) .toHaveTextContent( 'Select an item' ); await selectTester .selectOption({ option: 'Cat' }); expect(trigger) .toHaveTextContent( 'Cat' ); }); ### Properties | Name | Type | Description | | --- | --- | --- | | `trigger` | `HTMLElement` | Returns the select's trigger. | | `listbox` | `HTMLElement | null` | Returns the select's listbox if present. | | `sections` | `HTMLElement[]` | Returns the select's sections if present. | ### Methods | Method | Description | | --- | --- | | `constructor( (opts: SelectTesterOpts )): void` | | | `setInteractionType( (type: UserOpts ['interactionType'] )): void` | Set the interaction type used by the select tester. | | `open( (opts: SelectOpenOpts )): Promise<void>` | Opens the select. Defaults to using the interaction type set on the select tester. | | `close(): Promise<void>` | Closes the select. | | `findOption( (opts: { optionIndexOrText: number | | string } )): HTMLElement` | Returns a option matching the specified index or text content. | | `selectOption( (opts: SelectTriggerOptionOpts )): Promise<void>` | Selects the desired select option. Defaults to using the interaction type set on the select tester. If necessary, will open the select dropdown beforehand. The desired option can be targeted via the option's node, the option's text, or the option's index. | | `options( (opts: { element?: HTMLElement } )): HTMLElement[]` | Returns the select's options if present. Can be filtered to a subsection of the listbox if provided via `element`. | --- ## Page: https://react-spectrum.adobe.com/react-aria/Meter.html # Meter A meter represents a quantity within a known range, or a fractional value. <table><tbody><tr><th>install</th><td><code>yarn add react-aria-components</code></td></tr><tr><th>version</th><td>1.10.1</td></tr><tr><th>usage</th><td><code><span>import</span> {Meter} <span>from</span> <span>'react-aria-components'</span></code></td></tr></tbody></table> View ARIA pattern W3C View repository GitHub View package NPM ## Example# * * * import {Meter, Label} from 'react-aria-components'; <Meter value={25}> {({percentage, valueText}) => <> <Label>Storage space</Label> <span className="value">{valueText}</span> <div className="bar"> <div className="fill" style={{width: percentage + '%'}} /> </div> </>} </Meter> import {Label, Meter} from 'react-aria-components'; <Meter value={25}> {({ percentage, valueText }) => ( <> <Label>Storage space</Label> <span className="value">{valueText}</span> <div className="bar"> <div className="fill" style={{ width: percentage + '%' }} /> </div> </> )} </Meter> import { Label, Meter } from 'react-aria-components'; <Meter value={25}> {( { percentage, valueText } ) => ( <> <Label> Storage space </Label> <span className="value"> {valueText} </span> <div className="bar"> <div className="fill" style={{ width: percentage + '%' }} /> </div> </> )} </Meter> Storage space25% Show CSS @import "@react-aria/example-theme"; .react-aria-Meter { --fill-color: forestgreen; display: grid; grid-template-areas: "label value" "bar bar"; grid-template-columns: 1fr auto; gap: 4px; width: 250px; color: var(--text-color); .value { grid-area: value; } .bar { grid-area: bar; box-shadow: inset 0px 0px 0px 1px var(--border-color); forced-color-adjust: none; height: 10px; border-radius: 5px; overflow: hidden; } .fill { background: var(--fill-color); height: 100%; } } @media (forced-colors: active) { .react-aria-Meter { --fill-color: Highlight; } } @import "@react-aria/example-theme"; .react-aria-Meter { --fill-color: forestgreen; display: grid; grid-template-areas: "label value" "bar bar"; grid-template-columns: 1fr auto; gap: 4px; width: 250px; color: var(--text-color); .value { grid-area: value; } .bar { grid-area: bar; box-shadow: inset 0px 0px 0px 1px var(--border-color); forced-color-adjust: none; height: 10px; border-radius: 5px; overflow: hidden; } .fill { background: var(--fill-color); height: 100%; } } @media (forced-colors: active) { .react-aria-Meter { --fill-color: Highlight; } } @import "@react-aria/example-theme"; .react-aria-Meter { --fill-color: forestgreen; display: grid; grid-template-areas: "label value" "bar bar"; grid-template-columns: 1fr auto; gap: 4px; width: 250px; color: var(--text-color); .value { grid-area: value; } .bar { grid-area: bar; box-shadow: inset 0px 0px 0px 1px var(--border-color); forced-color-adjust: none; height: 10px; border-radius: 5px; overflow: hidden; } .fill { background: var(--fill-color); height: 100%; } } @media (forced-colors: active) { .react-aria-Meter { --fill-color: Highlight; } } ## Features# * * * The <meter> HTML element can be used to build a meter, however it is very difficult to style cross browser. `Meter` helps achieve accessible meters that can be styled as needed. * **Accessible** – Follows the ARIA meter pattern, with fallback to `progressbar` where unsupported. A nested label is automatically associated with the meter semantically. * **International** – The value is formatted as a percentage or custom format according to the user's locale. Note: Meters are similar to progress bars, but represent a quantity as opposed to progress over time. See ProgressBar for more details about progress bars. ## Anatomy# * * * Meters consist of a track element showing the full value in a range, a fill element showing the current value, a label, and an optional value label. The track and bar elements represent the value visually, while a wrapper element represents the meter to assistive technology using the meter ARIA role. import {Meter, Label} from 'react-aria-components'; <Meter> <Label /> </Meter> import {Meter, Label} from 'react-aria-components'; <Meter> <Label /> </Meter> import { Label, Meter } from 'react-aria-components'; <Meter> <Label /> </Meter> If there is no visual label, an `aria-label` or `aria-labelledby` prop must be passed instead to identify the element to screen readers. ### Composed components# A `Meter` uses the following components, which may also be used standalone or reused in other components. Label A label provides context for an element. ## Starter kits# * * * To help kick-start your project, we offer starter kits that include example implementations of all React Aria components with various styling solutions. All components are fully styled, including support for dark mode, high contrast mode, and all UI states. Each starter comes with a pre-configured Storybook that you can experiment with, or use as a starting point for your own component library. Vanilla CSS Download ZIP Preview Tailwind CSS Download ZIP Preview ## Reusable wrappers# * * * If you will use a Meter in multiple places in your app, you can wrap all of the pieces into a reusable component. This way, the DOM structure, styling code, and other logic are defined in a single place and reused everywhere to ensure consistency. This example wraps `Meter` and all of its children together into a single component which accepts a `label` prop that is passed to the right place. import type {MeterProps} from 'react-aria-components'; interface MyMeterProps extends MeterProps { label?: string } function MyMeter({label, ...props}: MyMeterProps) { return ( <Meter {...props}> {({percentage, valueText}) => <> <Label>{label}</Label> <span className="value">{valueText}</span> <div className="bar"> <div className="fill" style={{width: percentage + '%'}} /> </div> </>} </Meter> ); } <MyMeter label="Storage space" value={80} /> import type {MeterProps} from 'react-aria-components'; interface MyMeterProps extends MeterProps { label?: string; } function MyMeter({ label, ...props }: MyMeterProps) { return ( <Meter {...props}> {({ percentage, valueText }) => ( <> <Label>{label}</Label> <span className="value">{valueText}</span> <div className="bar"> <div className="fill" style={{ width: percentage + '%' }} /> </div> </> )} </Meter> ); } <MyMeter label="Storage space" value={80} /> import type {MeterProps} from 'react-aria-components'; interface MyMeterProps extends MeterProps { label?: string; } function MyMeter( { label, ...props }: MyMeterProps ) { return ( <Meter {...props}> {( { percentage, valueText } ) => ( <> <Label> {label} </Label> <span className="value"> {valueText} </span> <div className="bar"> <div className="fill" style={{ width: percentage + '%' }} /> </div> </> )} </Meter> ); } <MyMeter label="Storage space" value={80} /> Storage space80% ## Value# * * * Meters are controlled with the `value` prop. By default, the `value` prop represents the current percentage of progress, as the minimum and maximum values default to 0 and 100, respectively. <MyMeter label="Storage space" value={25} /> <MyMeter label="Storage space" value={25} /> <MyMeter label="Storage space" value={25} /> Storage space25% ### Custom value scale# A custom value scale can be used by setting the `minValue` and `maxValue` props. <MyMeter label="Widgets Used" minValue={50} maxValue={150} value={100} /> <MyMeter label="Widgets Used" minValue={50} maxValue={150} value={100} /> <MyMeter label="Widgets Used" minValue={50} maxValue={150} value={100} /> Widgets Used50% ## Labeling# * * * ### Value formatting# Values are formatted as a percentage by default, but this can be modified by using the `formatOptions` prop to specify a different format. `formatOptions` is compatible with the option parameter of Intl.NumberFormat and is applied based on the current locale. <MyMeter label="Currency" formatOptions={{style: 'currency', currency: 'JPY'}} value={60} /> <MyMeter label="Currency" formatOptions={{style: 'currency', currency: 'JPY'}} value={60} /> <MyMeter label="Currency" formatOptions={{ style: 'currency', currency: 'JPY' }} value={60} /> Currency¥60 ### Custom value label# The `valueLabel` prop allows the formatted value to be replaced with a custom string. <MyMeter label="Space used" valueLabel="54 of 60GB" value={90} /> <MyMeter label="Space used" valueLabel="54 of 60GB" value={90} /> <MyMeter label="Space used" valueLabel="54 of 60GB" value={90} /> Space used54 of 60GB ## Props# * * * | Name | Type | Default | Description | | --- | --- | --- | --- | | `formatOptions` | `Intl.NumberFormatOptions` | `{style: 'percent'}` | The display format of the value label. | | `valueLabel` | `ReactNode` | — | The content to display as the value's label (e.g. 1 of 4). | | `value` | `number` | `0` | The current value (controlled). | | `minValue` | `number` | `0` | The smallest value allowed for the input. | | `maxValue` | `number` | `100` | The largest value allowed for the input. | | `children` | `ReactNode | ( (values: MeterRenderProps & & { defaultChildren: ReactNode | | undefined } )) => ReactNode` | — | The children of the component. A function may be provided to alter the children based on component state. | | `className` | `string | ( (values: MeterRenderProps & & { defaultClassName: string | | undefined } )) => string` | — | The CSS className for the element. A function may be provided to compute the class based on component state. | | `style` | `CSSProperties | ( (values: MeterRenderProps & & { defaultStyle: CSSProperties } )) => CSSProperties | undefined` | — | The inline style for the element. A function may be provided to compute the style based on component state. | Layout | Name | Type | Description | | --- | --- | --- | | `slot` | `string | null` | A slot name for the component. Slots allow the component to receive props from a parent component. An explicit `null` value indicates that the local props completely override all props received from a parent. | Accessibility | Name | Type | Description | | --- | --- | --- | | `id` | `string` | The element's unique identifier. See MDN. | | `aria-label` | `string` | Defines a string value that labels the current element. | | `aria-labelledby` | `string` | Identifies the element (or elements) that labels the current element. | | `aria-describedby` | `string` | Identifies the element (or elements) that describes the object. | | `aria-details` | `string` | Identifies the element (or elements) that provide a detailed, extended description for the object. | ## Styling# * * * React Aria components can be styled in many ways, including using CSS classes, inline styles, utility classes (e.g. Tailwind), CSS-in-JS (e.g. Styled Components), etc. By default, all components include a builtin `className` attribute which can be targeted using CSS selectors. These follow the `react-aria-ComponentName` naming convention. .react-aria-Meter { /* ... */ } .react-aria-Meter { /* ... */ } .react-aria-Meter { /* ... */ } A custom `className` can also be specified on any component. This overrides the default `className` provided by React Aria with your own. <Meter className="my-meter"> {/* ... */} </Meter> <Meter className="my-meter"> {/* ... */} </Meter> <Meter className="my-meter"> {/* ... */} </Meter> The `className` and `style` props also accept functions which receive states for styling. This lets you dynamically determine the classes or styles to apply, which is useful when using utility CSS libraries like Tailwind. <Meter className={({ percentage }) => percentage > 50 ? 'bg-green-400' : 'bg-yellow-100'} > Item </Meter> <Meter className={({ percentage }) => percentage > 50 ? 'bg-green-400' : 'bg-yellow-100'} > Item </Meter> <Meter className={( { percentage } ) => percentage > 50 ? 'bg-green-400' : 'bg-yellow-100'} > Item </Meter> The selectors and render props for each component used in a `Meter` are documented below. ### Meter# A `Meter` can be targeted with the `.react-aria-Meter` CSS selector, or by overriding with a custom `className`. It supports the following states and render props: | Name | CSS Selector | Description | | --- | --- | --- | | `percentage` | `—` | The value as a percentage between the minimum and maximum. | | `valueText` | `[aria-valuetext]` | A formatted version of the value. | ### Label# A `Label` can be targeted with the `.react-aria-Label` CSS selector, or by adding a custom `className`. ## Advanced customization# * * * ### Contexts# All React Aria Components export a corresponding context that can be used to send props to them from a parent element. This enables you to build your own compositional APIs similar to those found in React Aria Components itself. You can send any prop or ref via context that you could pass to the corresponding component. The local props and ref on the component are merged with the ones passed via context, with the local props taking precedence (following the rules documented in mergeProps). | Component | Context | Props | Ref | | --- | --- | --- | --- | | `Meter` | `MeterContext` | ` MeterProps ` | `HTMLDivElement` | This example sets the `formatOptions` via context, which applies to all nested meters. import {MeterContext} from 'react-aria-components'; <MeterContext.Provider value={{formatOptions: {style: 'decimal'}}}> <MyMeter label="Widgets" value={28.5} /> <MyMeter label="Cookies" value={68.75} /> </MeterContext.Provider> import {MeterContext} from 'react-aria-components'; <MeterContext.Provider value={{ formatOptions: { style: 'decimal' } }} > <MyMeter label="Widgets" value={28.5} /> <MyMeter label="Cookies" value={68.75} /> </MeterContext.Provider> import {MeterContext} from 'react-aria-components'; <MeterContext.Provider value={{ formatOptions: { style: 'decimal' } }} > <MyMeter label="Widgets" value={28.5} /> <MyMeter label="Cookies" value={68.75} /> </MeterContext.Provider> Widgets28.5 Cookies68.75 ### Hooks# If you need to customize things further, such as customizing the DOM structure, you can drop down to the lower level Hook-based API. See useMeter for more details. --- ## Page: https://react-spectrum.adobe.com/react-aria/ProgressBar.html # ProgressBar Progress bars show either determinate or indeterminate progress of an operation over time. <table><tbody><tr><th>install</th><td><code>yarn add react-aria-components</code></td></tr><tr><th>version</th><td>1.10.1</td></tr><tr><th>usage</th><td><code><span>import</span> {ProgressBar} <span>from</span> <span>'react-aria-components'</span></code></td></tr></tbody></table> View ARIA pattern W3C View repository GitHub View package NPM ## Example# * * * import {ProgressBar, Label} from 'react-aria-components'; <ProgressBar value={80}> {({percentage, valueText}) => <> <Label>Loading…</Label> <span className="value">{valueText}</span> <div className="bar"> <div className="fill" style={{width: percentage + '%'}} /> </div> </>} </ProgressBar> import {Label, ProgressBar} from 'react-aria-components'; <ProgressBar value={80}> {({ percentage, valueText }) => ( <> <Label>Loading…</Label> <span className="value">{valueText}</span> <div className="bar"> <div className="fill" style={{ width: percentage + '%' }} /> </div> </> )} </ProgressBar> import { Label, ProgressBar } from 'react-aria-components'; <ProgressBar value={80} > {( { percentage, valueText } ) => ( <> <Label> Loading… </Label> <span className="value"> {valueText} </span> <div className="bar"> <div className="fill" style={{ width: percentage + '%' }} /> </div> </> )} </ProgressBar> Loading…80% Show CSS @import "@react-aria/example-theme"; .react-aria-ProgressBar { display: grid; grid-template-areas: "label value" "bar bar"; grid-template-columns: 1fr auto; gap: 4px; width: 250px; color: var(--text-color); .value { grid-area: value; } .bar { grid-area: bar; box-shadow: inset 0px 0px 0px 1px var(--border-color); forced-color-adjust: none; height: 10px; border-radius: 5px; overflow: hidden; will-change: transform; } .fill { background: var(--highlight-background); height: 100%; } } @import "@react-aria/example-theme"; .react-aria-ProgressBar { display: grid; grid-template-areas: "label value" "bar bar"; grid-template-columns: 1fr auto; gap: 4px; width: 250px; color: var(--text-color); .value { grid-area: value; } .bar { grid-area: bar; box-shadow: inset 0px 0px 0px 1px var(--border-color); forced-color-adjust: none; height: 10px; border-radius: 5px; overflow: hidden; will-change: transform; } .fill { background: var(--highlight-background); height: 100%; } } @import "@react-aria/example-theme"; .react-aria-ProgressBar { display: grid; grid-template-areas: "label value" "bar bar"; grid-template-columns: 1fr auto; gap: 4px; width: 250px; color: var(--text-color); .value { grid-area: value; } .bar { grid-area: bar; box-shadow: inset 0px 0px 0px 1px var(--border-color); forced-color-adjust: none; height: 10px; border-radius: 5px; overflow: hidden; will-change: transform; } .fill { background: var(--highlight-background); height: 100%; } } ## Features# * * * The <progress> HTML element can be used to build a progress bar, however it is very difficult to style cross browser. `ProgressBar` helps achieve accessible progress bars and spinners that can be styled as needed. * **Accessible** – Follows the ARIA progressbar pattern, supporting both determinate and indeterminate progress bars. A nested label is automatically associated with the progress bar semantically. * **International** – The value is formatted as a percentage or custom format according to the user's locale. ## Anatomy# * * * Progress bars consist of a track element showing the full progress of an operation, a fill element showing the current progress, a label, and an optional value label. The track and bar elements represent the progress visually, while a wrapper element represents the progress to assistive technology using the progressbar ARIA role. import {ProgressBar, Label} from 'react-aria-components'; <ProgressBar> <Label /> </ProgressBar> import {ProgressBar, Label} from 'react-aria-components'; <ProgressBar> <Label /> </ProgressBar> import { Label, ProgressBar } from 'react-aria-components'; <ProgressBar> <Label /> </ProgressBar> If there is no visual label, an `aria-label` or `aria-labelledby` prop must be passed instead to identify the element to screen readers. ### Composed components# A `ProgressBar` uses the following components, which may also be used standalone or reused in other components. Label A label provides context for an element. ## Examples# * * * Loading ProgressBar A loading ProgressBar styled with Tailwind CSS. ## Starter kits# * * * To help kick-start your project, we offer starter kits that include example implementations of all React Aria components with various styling solutions. All components are fully styled, including support for dark mode, high contrast mode, and all UI states. Each starter comes with a pre-configured Storybook that you can experiment with, or use as a starting point for your own component library. Vanilla CSS Download ZIP Preview Tailwind CSS Download ZIP Preview ## Reusable wrappers# * * * If you will use a ProgressBar in multiple places in your app, you can wrap all of the pieces into a reusable component. This way, the DOM structure, styling code, and other logic are defined in a single place and reused everywhere to ensure consistency. This example wraps `ProgressBar` and all of its children together into a single component which accepts a `label` prop that is passed to the right place. import type {ProgressBarProps} from 'react-aria-components'; interface MyProgressBarProps extends ProgressBarProps { label?: string } function MyProgressBar({label, ...props}: MyProgressBarProps) { return ( <ProgressBar {...props}> {({percentage, valueText}) => <> <Label>{label}</Label> <span className="value">{valueText}</span> <div className="bar"> <div className="fill" style={{width: percentage + '%'}} /> </div> </>} </ProgressBar> ); } <MyProgressBar label="Loading…" value={80} /> import type {ProgressBarProps} from 'react-aria-components'; interface MyProgressBarProps extends ProgressBarProps { label?: string; } function MyProgressBar( { label, ...props }: MyProgressBarProps ) { return ( <ProgressBar {...props}> {({ percentage, valueText }) => ( <> <Label>{label}</Label> <span className="value">{valueText}</span> <div className="bar"> <div className="fill" style={{ width: percentage + '%' }} /> </div> </> )} </ProgressBar> ); } <MyProgressBar label="Loading…" value={80} /> import type {ProgressBarProps} from 'react-aria-components'; interface MyProgressBarProps extends ProgressBarProps { label?: string; } function MyProgressBar( { label, ...props }: MyProgressBarProps ) { return ( <ProgressBar {...props} > {( { percentage, valueText } ) => ( <> <Label> {label} </Label> <span className="value"> {valueText} </span> <div className="bar"> <div className="fill" style={{ width: percentage + '%' }} /> </div> </> )} </ProgressBar> ); } <MyProgressBar label="Loading…" value={80} /> Loading…80% ## Value# * * * ProgressBars are controlled with the `value` prop. By default, the `value` prop represents the current percentage of progress, as the minimum and maximum values default to 0 and 100, respectively. <MyProgressBar label="Loading…" value={25} /> <MyProgressBar label="Loading…" value={25} /> <MyProgressBar label="Loading…" value={25} /> Loading…25% ### Custom value scale# A custom value scale can be used by setting the `minValue` and `maxValue` props. <MyProgressBar label="Loading…" minValue={50} maxValue={150} value={100} /> <MyProgressBar label="Loading…" minValue={50} maxValue={150} value={100} /> <MyProgressBar label="Loading…" minValue={50} maxValue={150} value={100} /> Loading…50% ### Indeterminate# The `isIndeterminate` prop can be used to represent an indeterminate operation. <MyProgressBar aria-label="Loading…" isIndeterminate /> <MyProgressBar aria-label="Loading…" isIndeterminate /> <MyProgressBar aria-label="Loading…" isIndeterminate /> Show CSS .react-aria-ProgressBar { &:not([aria-valuenow]) { .fill { width: 120px; border-radius: inherit; animation: indeterminate 1.5s infinite ease-in-out; will-change: transform; } } } @keyframes indeterminate { from { transform: translateX(-100%); } to { transform: translateX(250px); } } .react-aria-ProgressBar { &:not([aria-valuenow]) { .fill { width: 120px; border-radius: inherit; animation: indeterminate 1.5s infinite ease-in-out; will-change: transform; } } } @keyframes indeterminate { from { transform: translateX(-100%); } to { transform: translateX(250px); } } .react-aria-ProgressBar { &:not([aria-valuenow]) { .fill { width: 120px; border-radius: inherit; animation: indeterminate 1.5s infinite ease-in-out; will-change: transform; } } } @keyframes indeterminate { from { transform: translateX(-100%); } to { transform: translateX(250px); } } ## Labeling# * * * ### Value formatting# Values are formatted as a percentage by default, but this can be modified by using the `formatOptions` prop to specify a different format. `formatOptions` is compatible with the option parameter of Intl.NumberFormat and is applied based on the current locale. <MyProgressBar label="Sending…" formatOptions={{style: 'currency', currency: 'JPY'}} value={60} /> <MyProgressBar label="Sending…" formatOptions={{style: 'currency', currency: 'JPY'}} value={60} /> <MyProgressBar label="Sending…" formatOptions={{ style: 'currency', currency: 'JPY' }} value={60} /> Sending…¥60 ### Custom value label# The `valueLabel` prop allows the formatted value to be replaced with a custom string. <MyProgressBar label="Feeding…" valueLabel="30 of 100 dogs" value={30} /> <MyProgressBar label="Feeding…" valueLabel="30 of 100 dogs" value={30} /> <MyProgressBar label="Feeding…" valueLabel="30 of 100 dogs" value={30} /> Feeding…30 of 100 dogs ## Circular# * * * Progress bars may also be represented using a circular visualization rather than a line. This is often used to represent indeterminate operations, but may also be used for determinate progress indicators when space is limited. The following example shows a progress bar visualized as a circular spinner using SVG. let center = 16; let strokeWidth = 4; let r = 16 - strokeWidth; let c = 2 * r * Math.PI; <ProgressBar aria-label="Loading…" value={60}> {({ percentage }) => ( <> <svg width={64} height={64} viewBox="0 0 32 32" fill="none" strokeWidth={strokeWidth} > <circle cx={center} cy={center} r={r - (strokeWidth / 2 - 0.25)} stroke="var(--border-color)" strokeWidth={0.5} /> <circle cx={center} cy={center} r={r + (strokeWidth / 2 - 0.25)} stroke="var(--border-color)" strokeWidth={0.5} /> <circle cx={center} cy={center} r={r} stroke="var(--highlight-background)" strokeDasharray={`${c} ${c}`} strokeDashoffset={c - percentage / 100 * c} strokeLinecap="round" transform="rotate(-90 16 16)" /> </svg> </> )} </ProgressBar> let center = 16; let strokeWidth = 4; let r = 16 - strokeWidth; let c = 2 * r * Math.PI; <ProgressBar aria-label="Loading…" value={60}> {({ percentage }) => ( <> <svg width={64} height={64} viewBox="0 0 32 32" fill="none" strokeWidth={strokeWidth} > <circle cx={center} cy={center} r={r - (strokeWidth / 2 - 0.25)} stroke="var(--border-color)" strokeWidth={0.5} /> <circle cx={center} cy={center} r={r + (strokeWidth / 2 - 0.25)} stroke="var(--border-color)" strokeWidth={0.5} /> <circle cx={center} cy={center} r={r} stroke="var(--highlight-background)" strokeDasharray={`${c} ${c}`} strokeDashoffset={c - percentage / 100 * c} strokeLinecap="round" transform="rotate(-90 16 16)" /> </svg> </> )} </ProgressBar> let center = 16; let strokeWidth = 4; let r = 16 - strokeWidth; let c = 2 * r * Math.PI; <ProgressBar aria-label="Loading…" value={60} > {( { percentage } ) => ( <> <svg width={64} height={64} viewBox="0 0 32 32" fill="none" strokeWidth={strokeWidth} > <circle cx={center} cy={center} r={r - (strokeWidth / 2 - 0.25)} stroke="var(--border-color)" strokeWidth={0.5} /> <circle cx={center} cy={center} r={r + (strokeWidth / 2 - 0.25)} stroke="var(--border-color)" strokeWidth={0.5} /> <circle cx={center} cy={center} r={r} stroke="var(--highlight-background)" strokeDasharray={`${c} ${c}`} strokeDashoffset={c - percentage / 100 * c} strokeLinecap="round" transform="rotate(-90 16 16)" /> </svg> </> )} </ProgressBar> ## Props# * * * | Name | Type | Default | Description | | --- | --- | --- | --- | | `isIndeterminate` | `boolean` | — | Whether presentation is indeterminate when progress isn't known. | | `formatOptions` | `Intl.NumberFormatOptions` | `{style: 'percent'}` | The display format of the value label. | | `valueLabel` | `ReactNode` | — | The content to display as the value's label (e.g. 1 of 4). | | `value` | `number` | `0` | The current value (controlled). | | `minValue` | `number` | `0` | The smallest value allowed for the input. | | `maxValue` | `number` | `100` | The largest value allowed for the input. | | `children` | `ReactNode | ( (values: ProgressBarRenderProps & & { defaultChildren: ReactNode | | undefined } )) => ReactNode` | — | The children of the component. A function may be provided to alter the children based on component state. | | `className` | `string | ( (values: ProgressBarRenderProps & & { defaultClassName: string | | undefined } )) => string` | — | The CSS className for the element. A function may be provided to compute the class based on component state. | | `style` | `CSSProperties | ( (values: ProgressBarRenderProps & & { defaultStyle: CSSProperties } )) => CSSProperties | undefined` | — | The inline style for the element. A function may be provided to compute the style based on component state. | Layout | Name | Type | Description | | --- | --- | --- | | `slot` | `string | null` | A slot name for the component. Slots allow the component to receive props from a parent component. An explicit `null` value indicates that the local props completely override all props received from a parent. | Accessibility | Name | Type | Description | | --- | --- | --- | | `id` | `string` | The element's unique identifier. See MDN. | | `aria-label` | `string` | Defines a string value that labels the current element. | | `aria-labelledby` | `string` | Identifies the element (or elements) that labels the current element. | | `aria-describedby` | `string` | Identifies the element (or elements) that describes the object. | | `aria-details` | `string` | Identifies the element (or elements) that provide a detailed, extended description for the object. | ## Styling# * * * React Aria components can be styled in many ways, including using CSS classes, inline styles, utility classes (e.g. Tailwind), CSS-in-JS (e.g. Styled Components), etc. By default, all components include a builtin `className` attribute which can be targeted using CSS selectors. These follow the `react-aria-ComponentName` naming convention. .react-aria-ProgressBar { /* ... */ } .react-aria-ProgressBar { /* ... */ } .react-aria-ProgressBar { /* ... */ } A custom `className` can also be specified on any component. This overrides the default `className` provided by React Aria with your own. <ProgressBar className="my-progressbar"> {/* ... */} </ProgressBar> <ProgressBar className="my-progressbar"> {/* ... */} </ProgressBar> <ProgressBar className="my-progressbar"> {/* ... */} </ProgressBar> The `className` and `style` props also accept functions which receive states for styling. This lets you dynamically determine the classes or styles to apply, which is useful when using utility CSS libraries like Tailwind. <ProgressBar className={({ percentage }) => percentage > 50 ? 'bg-green-400' : 'bg-yellow-100'} > Item </ProgressBar> <ProgressBar className={({ percentage }) => percentage > 50 ? 'bg-green-400' : 'bg-yellow-100'} > Item </ProgressBar> <ProgressBar className={( { percentage } ) => percentage > 50 ? 'bg-green-400' : 'bg-yellow-100'} > Item </ProgressBar> The selectors and render props for each component used in a `ProgressBar` are documented below. ### ProgressBar# A `ProgressBar` can be targeted with the `.react-aria-ProgressBar` CSS selector, or by overriding with a custom `className`. It supports the following states and render props: | Name | CSS Selector | Description | | --- | --- | --- | | `valueText` | `[aria-valuetext]` | A formatted version of the value. | | `isIndeterminate` | `:not([aria-valuenow])` | Whether the progress bar is indeterminate. | ### Label# A `Label` can be targeted with the `.react-aria-Label` CSS selector, or by overriding with a custom `className`. ## Advanced customization# * * * ### Contexts# All React Aria Components export a corresponding context that can be used to send props to them from a parent element. This enables you to build your own compositional APIs similar to those found in React Aria Components itself. You can send any prop or ref via context that you could pass to the corresponding component. The local props and ref on the component are merged with the ones passed via context, with the local props taking precedence (following the rules documented in mergeProps). | Component | Context | Props | Ref | | --- | --- | --- | --- | | `ProgressBar` | `ProgressBarContext` | ` ProgressBarProps ` | `HTMLDivElement` | This example sets the `formatOptions` via context, which applies to all nested progress bars. import {ProgressBarContext} from 'react-aria-components'; <ProgressBarContext.Provider value={{ formatOptions: { style: 'decimal' } }}> <MyProgressBar label="Converting…" value={28.5} /> <MyProgressBar label="Uploading…" value={68.75} /> </ProgressBarContext.Provider> import {ProgressBarContext} from 'react-aria-components'; <ProgressBarContext.Provider value={{ formatOptions: { style: 'decimal' } }} > <MyProgressBar label="Converting…" value={28.5} /> <MyProgressBar label="Uploading…" value={68.75} /> </ProgressBarContext.Provider> import {ProgressBarContext} from 'react-aria-components'; <ProgressBarContext.Provider value={{ formatOptions: { style: 'decimal' } }} > <MyProgressBar label="Converting…" value={28.5} /> <MyProgressBar label="Uploading…" value={68.75} /> </ProgressBarContext.Provider> Converting…28.5 Uploading…68.75 ### Hooks# If you need to customize things further, such as customizing the DOM structure, you can drop down to the lower level Hook-based API. See useProgressBar for more details. --- ## Page: https://react-spectrum.adobe.com/react-aria/Toast.html alpha # Toast A Toast displays a brief, temporary notification of actions, errors, or other events in an application. <table><tbody><tr><th>install</th><td><code>yarn add react-aria-components</code></td></tr><tr><th>version</th><td>1.10.1</td></tr><tr><th>usage</th><td><code><span>import</span> {ToastRegion, Toast} <span>from</span> <span>'react-aria-components'</span></code></td></tr></tbody></table> View ARIA pattern W3C View repository GitHub View package NPM ### Under construction This component is in **alpha**. More documentation is coming soon! ## Example# * * * First, render a `ToastRegion` in the root of your app. import {Button, Text, UNSTABLE_Toast as Toast, UNSTABLE_ToastContent as ToastContent, UNSTABLE_ToastQueue as ToastQueue, UNSTABLE_ToastRegion as ToastRegion} from 'react-aria-components'; // Define the type for your toast content. interface MyToastContent { title: string; description?: string; } // Create a global ToastQueue. export const queue = new ToastQueue<MyToastContent>(); // Render a <ToastRegion> in the root of your app. export function App() { return ( <> <ToastRegion queue={queue}> {({ toast }) => ( <Toast toast={toast}> <ToastContent> <Text slot="title">{toast.content.title}</Text> <Text slot="description">{toast.content.description}</Text> </ToastContent> <Button slot="close">x</Button> </Toast> )} </ToastRegion> {/* Your app here */} </> ); } import { Button, Text, UNSTABLE_Toast as Toast, UNSTABLE_ToastContent as ToastContent, UNSTABLE_ToastQueue as ToastQueue, UNSTABLE_ToastRegion as ToastRegion } from 'react-aria-components'; // Define the type for your toast content. interface MyToastContent { title: string; description?: string; } // Create a global ToastQueue. export const queue = new ToastQueue<MyToastContent>(); // Render a <ToastRegion> in the root of your app. export function App() { return ( <> <ToastRegion queue={queue}> {({ toast }) => ( <Toast toast={toast}> <ToastContent> <Text slot="title"> {toast.content.title} </Text> <Text slot="description"> {toast.content.description} </Text> </ToastContent> <Button slot="close">x</Button> </Toast> )} </ToastRegion> {/* Your app here */} </> ); } import { Button, Text, UNSTABLE_Toast as Toast, UNSTABLE_ToastContent as ToastContent, UNSTABLE_ToastQueue as ToastQueue, UNSTABLE_ToastRegion as ToastRegion } from 'react-aria-components'; // Define the type for your toast content. interface MyToastContent { title: string; description?: string; } // Create a global ToastQueue. export const queue = new ToastQueue< MyToastContent >(); // Render a <ToastRegion> in the root of your app. export function App() { return ( <> <ToastRegion queue={queue} > {({ toast }) => ( <Toast toast={toast} > <ToastContent> <Text slot="title"> {toast .content .title} </Text> <Text slot="description"> {toast .content .description} </Text> </ToastContent> <Button slot="close"> x </Button> </Toast> )} </ToastRegion> {/* Your app here */} </> ); } Then, you can trigger a toast from anywhere using the exported `queue`. <Button onPress={() => queue.add({ title: 'Toast complete!', description: 'Great success.' })}> Toast </Button> <Button onPress={() => queue.add({ title: 'Toast complete!', description: 'Great success.' })}> Toast </Button> <Button onPress={() => queue.add({ title: 'Toast complete!', description: 'Great success.' })} > Toast </Button> Toast Show CSS @import "@react-aria/example-theme"; .react-aria-ToastRegion { position: fixed; bottom: 16px; right: 16px; display: flex; flex-direction: column-reverse; gap: 8px; border-radius: 8px; outline: none; &[data-focus-visible] { outline: 2px solid slateblue; outline-offset: 2px; } } .react-aria-Toast { display: flex; align-items: center; gap: 16px; background: slateblue; color: white; padding: 12px 16px; border-radius: 8px; outline: none; &[data-focus-visible] { outline: 2px solid slateblue; outline-offset: 2px; } .react-aria-ToastContent { display: flex; flex-direction: column; flex: 1 1 auto; min-width: 0px; [slot=title] { font-weight: bold; } } .react-aria-Button[slot=close] { flex: 0 0 auto; background: none; border: none; appearance: none; border-radius: 50%; height: 32px; width: 32px; font-size: 16px; border: 1px solid white; color: white; padding: 0; outline: none; &[data-focus-visible] { box-shadow: 0 0 0 2px slateblue, 0 0 0 4px white; } &[data-pressed] { background: rgba(255, 255, 255, 0.2); } } } @import "@react-aria/example-theme"; .react-aria-ToastRegion { position: fixed; bottom: 16px; right: 16px; display: flex; flex-direction: column-reverse; gap: 8px; border-radius: 8px; outline: none; &[data-focus-visible] { outline: 2px solid slateblue; outline-offset: 2px; } } .react-aria-Toast { display: flex; align-items: center; gap: 16px; background: slateblue; color: white; padding: 12px 16px; border-radius: 8px; outline: none; &[data-focus-visible] { outline: 2px solid slateblue; outline-offset: 2px; } .react-aria-ToastContent { display: flex; flex-direction: column; flex: 1 1 auto; min-width: 0px; [slot=title] { font-weight: bold; } } .react-aria-Button[slot=close] { flex: 0 0 auto; background: none; border: none; appearance: none; border-radius: 50%; height: 32px; width: 32px; font-size: 16px; border: 1px solid white; color: white; padding: 0; outline: none; &[data-focus-visible] { box-shadow: 0 0 0 2px slateblue, 0 0 0 4px white; } &[data-pressed] { background: rgba(255, 255, 255, 0.2); } } } @import "@react-aria/example-theme"; .react-aria-ToastRegion { position: fixed; bottom: 16px; right: 16px; display: flex; flex-direction: column-reverse; gap: 8px; border-radius: 8px; outline: none; &[data-focus-visible] { outline: 2px solid slateblue; outline-offset: 2px; } } .react-aria-Toast { display: flex; align-items: center; gap: 16px; background: slateblue; color: white; padding: 12px 16px; border-radius: 8px; outline: none; &[data-focus-visible] { outline: 2px solid slateblue; outline-offset: 2px; } .react-aria-ToastContent { display: flex; flex-direction: column; flex: 1 1 auto; min-width: 0px; [slot=title] { font-weight: bold; } } .react-aria-Button[slot=close] { flex: 0 0 auto; background: none; border: none; appearance: none; border-radius: 50%; height: 32px; width: 32px; font-size: 16px; border: 1px solid white; color: white; padding: 0; outline: none; &[data-focus-visible] { box-shadow: 0 0 0 2px slateblue, 0 0 0 4px white; } &[data-pressed] { background: rgba(255, 255, 255, 0.2); } } } ## Features# * * * There is no built in way to display toast notifications in HTML. `<ToastRegion>` and `<Toast>` help achieve accessible toasts that can be styled as needed. * **Accessible** – Toasts follow the ARIA alertdialog pattern. They are rendered in a landmark region, which keyboard and screen reader users can easily jump to when an alert is announced. * **Focus management** – When a toast unmounts, focus is moved to the next toast if any. Otherwise, focus is restored to where it was before navigating to the toast region. ## Anatomy# * * * A `<ToastRegion>` is an ARIA landmark region labeled "Notifications" by default. A `<ToastRegion>` accepts a function to render one or more visible toasts, in chronological order. When the limit is reached, additional toasts are queued until the user dismisses one. Each `<Toast>` is a non-modal ARIA alertdialog, containing the content of the notification and a close button. Landmark regions including the toast container can be navigated using the keyboard by pressing the F6 key to move forward, and the Shift + F6 key to move backward. This provides an easy way for keyboard users to jump to the toasts from anywhere in the app. When the last toast is closed, keyboard focus is restored. import {Button, Text, Toast, ToastContent, ToastRegion} from 'react-aria-components'; <ToastRegion> {({ toast }) => ( <Toast toast={toast}> <ToastContent> <Text slot="title" /> <Text slot="description" /> </ToastContent> <Button slot="close" /> </Toast> )} </ToastRegion> import { Button, Text, Toast, ToastContent, ToastRegion } from 'react-aria-components'; <ToastRegion> {({ toast }) => ( <Toast toast={toast}> <ToastContent> <Text slot="title" /> <Text slot="description" /> </ToastContent> <Button slot="close" /> </Toast> )} </ToastRegion> import { Button, Text, Toast, ToastContent, ToastRegion } from 'react-aria-components'; <ToastRegion> {({ toast }) => ( <Toast toast={toast} > <ToastContent> <Text slot="title" /> <Text slot="description" /> </ToastContent> <Button slot="close" /> </Toast> )} </ToastRegion> ## Auto-dismiss# * * * Toasts support a `timeout` option to automatically hide them after a certain amount of time. For accessibility, toasts should have a minimum timeout of 5 seconds to give users enough time to read them. If a toast includes action buttons or other interactive elements it should not auto dismiss. In addition, timers will automatically pause when the user focuses or hovers over a toast. Be sure only to automatically dismiss toasts when the information is not important, or may be found elsewhere. Some users may require additional time to read a toast message, and screen zoom users may miss toasts entirely. <Button onPress={() => queue.add({title: 'Toast is done!'}, {timeout: 5000})}> Show toast </Button> <Button onPress={() => queue.add({ title: 'Toast is done!' }, { timeout: 5000 })}> Show toast </Button> <Button onPress={() => queue.add({ title: 'Toast is done!' }, { timeout: 5000 })}> Show toast </Button> Show toast ## Programmatic dismissal# * * * Toasts may be programmatically dismissed if they become irrelevant before the user manually closes them. `queue.add` returns a key for the toast which may be passed to `queue.close` to dismiss the toast. function Example() { let [toastKey, setToastKey] = React.useState(null); return ( <Button onPress={() => { if (!toastKey) { setToastKey(queue.add({ title: 'Unable to save' }, { onClose: () => setToastKey(null) })); } else { queue.close(toastKey); } }} > {toastKey ? 'Hide' : 'Show'} Toast </Button> ); } function Example() { let [toastKey, setToastKey] = React.useState(null); return ( <Button onPress={() => { if (!toastKey) { setToastKey( queue.add({ title: 'Unable to save' }, { onClose: () => setToastKey(null) }) ); } else { queue.close(toastKey); } }} > {toastKey ? 'Hide' : 'Show'} Toast </Button> ); } function Example() { let [ toastKey, setToastKey ] = React.useState( null ); return ( <Button onPress={() => { if (!toastKey) { setToastKey( queue.add({ title: 'Unable to save' }, { onClose: () => setToastKey( null ) }) ); } else { queue.close( toastKey ); } }} > {toastKey ? 'Hide' : 'Show'} Toast </Button> ); } Show Toast ## Animations# * * * Toast entry and exit animations can be done using third party animation libraries like Motion, or using native CSS view transitions. This example shows how to use the `wrapUpdate` option of `ToastQueue` to wrap state updates in a CSS view transition. The `toast.key` can be used to assign a `viewTransitionName` to each `Toast`. import {flushSync} from 'react-dom'; const queue = new ToastQueue<MyToastContent>({ // Wrap state updates in a CSS view transition. wrapUpdate(fn) { if ('startViewTransition' in document) { document.startViewTransition(() => { flushSync(fn); }); } else { fn(); } }}); <ToastRegion queue={queue}> {({toast}) => ( <Toast style={{viewTransitionName: toast.key}} toast={toast}> <ToastContent> <Text slot="title">{toast.content.title}</Text> <Text slot="description">{toast.content.description}</Text> </ToastContent> <Button slot="close">x</Button> </Toast> )} </ToastRegion> <Button onPress={() => queue.add({title: 'Toasted!'})}>Toast</Button> import {flushSync} from 'react-dom'; const queue = new ToastQueue<MyToastContent>({ // Wrap state updates in a CSS view transition. wrapUpdate(fn) { if ('startViewTransition' in document) { document.startViewTransition(() => { flushSync(fn); }); } else { fn(); } }}); <ToastRegion queue={queue}> {({ toast }) => ( <Toast style={{ viewTransitionName: toast.key }} toast={toast} > <ToastContent> <Text slot="title">{toast.content.title}</Text> <Text slot="description"> {toast.content.description} </Text> </ToastContent> <Button slot="close">x</Button> </Toast> )} </ToastRegion> <Button onPress={() => queue.add({ title: 'Toasted!' })}> Toast </Button> import {flushSync} from 'react-dom'; const queue = new ToastQueue< MyToastContent >({ // Wrap state updates in a CSS view transition. wrapUpdate(fn) { if ( 'startViewTransition' in document ) { document .startViewTransition( () => { flushSync( fn ); } ); } else { fn(); } } }); <ToastRegion queue={queue} > {({ toast }) => ( <Toast style={{ viewTransitionName: toast.key }} toast={toast} > <ToastContent> <Text slot="title"> {toast .content .title} </Text> <Text slot="description"> {toast .content .description} </Text> </ToastContent> <Button slot="close"> x </Button> </Toast> )} </ToastRegion> <Button onPress={() => queue.add({ title: 'Toasted!' })} > Toast </Button> Toast Show CSS .react-aria-Toast { view-transition-class: toast; } ::view-transition-new(.toast):only-child { animation: slide-in 400ms; } ::view-transition-old(.toast):only-child { animation: slide-out 400ms; } @keyframes slide-out { to { translate: 100% 0; opacity: 0; } } @keyframes slide-in { from { translate: 100% 0; opacity: 0; } } .react-aria-Toast { view-transition-class: toast; } ::view-transition-new(.toast):only-child { animation: slide-in 400ms; } ::view-transition-old(.toast):only-child { animation: slide-out 400ms; } @keyframes slide-out { to { translate: 100% 0; opacity: 0; } } @keyframes slide-in { from { translate: 100% 0; opacity: 0; } } .react-aria-Toast { view-transition-class: toast; } ::view-transition-new(.toast):only-child { animation: slide-in 400ms; } ::view-transition-old(.toast):only-child { animation: slide-out 400ms; } @keyframes slide-out { to { translate: 100% 0; opacity: 0; } } @keyframes slide-in { from { translate: 100% 0; opacity: 0; } } ## Props# * * * ### ToastRegion# `<ToastRegion>` renders a group of toasts. | Name | Type | Description | | --- | --- | --- | | `queue` | ` ToastQueue <T>` | The queue of toasts to display. | | `children` | `ReactNode | ( (renderProps: { toast: QueuedToast <T> } )) => ReactElement` | A function to render each toast, or children containing a `<ToastList>`. | | `className` | `string | ( (values: ToastRegionRenderProps <T> & & { defaultClassName: string | | undefined } )) => string` | The CSS className for the element. A function may be provided to compute the class based on component state. | | `style` | `CSSProperties | ( (values: ToastRegionRenderProps <T> & & { defaultStyle: CSSProperties } )) => CSSProperties | undefined` | The inline style for the element. A function may be provided to compute the style based on component state. | Accessibility | Name | Type | Default | Description | | --- | --- | --- | --- | | `aria-label` | `string` | `"Notifications"` | An accessibility label for the toast region. | | `aria-labelledby` | `string` | — | Identifies the element (or elements) that labels the current element. | | `aria-describedby` | `string` | — | Identifies the element (or elements) that describes the object. | | `aria-details` | `string` | — | Identifies the element (or elements) that provide a detailed, extended description for the object. | ### Toast# `<Toast>` renders an individual toast. | Name | Type | Description | | --- | --- | --- | | `toast` | ` QueuedToast <T>` | The toast object. | | `children` | `ReactNode | ( (values: ToastRenderProps <T> & & { defaultChildren: ReactNode | | undefined } )) => ReactNode` | The children of the component. A function may be provided to alter the children based on component state. | | `className` | `string | ( (values: ToastRenderProps <T> & & { defaultClassName: string | | undefined } )) => string` | The CSS className for the element. A function may be provided to compute the class based on component state. | | `style` | `CSSProperties | ( (values: ToastRenderProps <T> & & { defaultStyle: CSSProperties } )) => CSSProperties | undefined` | The inline style for the element. A function may be provided to compute the style based on component state. | Accessibility | Name | Type | Description | | --- | --- | --- | | `aria-label` | `string` | Defines a string value that labels the current element. | | `aria-labelledby` | `string` | Identifies the element (or elements) that labels the current element. | | `aria-describedby` | `string` | Identifies the element (or elements) that describes the object. | | `aria-details` | `string` | Identifies the element (or elements) that provide a detailed, extended description for the object. | ### ToastContent# `<ToastContent>` renders the main content of a toast, including the title and description. It accepts all HTML attributes. ## ToastQueue API# * * * A `ToastQueue` manages the state for a `<ToastRegion>`. The state is stored outside React so that you can trigger toasts from anywhere in your application, not just inside components. ### Properties | Name | Type | Description | | --- | --- | --- | | `visibleToasts` | ` QueuedToast <T>[]` | The currently visible toasts. | ### Methods | Method | Description | | --- | --- | | `constructor( (options?: ToastStateProps )): void` | | | `subscribe( (fn: () => void )): () => void` | Subscribes to updates to the visible toasts. | | `add( (content: T, , options: ToastOptions )): string` | Adds a new toast to the queue. | | `close( (key: string )): void` | Closes a toast. | | `pauseAll(): void` | Pauses the timers for all visible toasts. | | `resumeAll(): void` | Resumes the timers for all visible toasts. | | `clear(): void` | | --- ## Page: https://react-spectrum.adobe.com/react-aria/Group.html # Group A group represents a set of related UI controls, and supports interactive states for styling. <table><tbody><tr><th>install</th><td><code>yarn add react-aria-components</code></td></tr><tr><th>version</th><td>1.10.1</td></tr><tr><th>usage</th><td><code><span>import</span> {Group} <span>from</span> <span>'react-aria-components'</span></code></td></tr></tbody></table> View ARIA pattern W3C View repository GitHub View package NPM ## Example# * * * import {TextField, Label, Group, Input, Button} from 'react-aria-components'; <TextField> <Label>Email</Label> <Group> <Input /> <Button aria-label="Add email">+</Button> </Group> </TextField> import { Button, Group, Input, Label, TextField } from 'react-aria-components'; <TextField> <Label>Email</Label> <Group> <Input /> <Button aria-label="Add email">+</Button> </Group> </TextField> import { Button, Group, Input, Label, TextField } from 'react-aria-components'; <TextField> <Label>Email</Label> <Group> <Input /> <Button aria-label="Add email"> + </Button> </Group> </TextField> Email + Show CSS @import "@react-aria/example-theme"; .react-aria-Group { display: flex; align-items: center; width: fit-content; border-radius: 6px; border: 1px solid var(--border-color); background: var(--field-background); overflow: hidden; transition: all 200ms; &[data-hovered] { border-color: var(--border-color-hover); } &[data-focus-within] { outline: 2px solid var(--focus-ring-color); outline-offset: -1px; } .react-aria-Input { padding: 0.286rem; margin: 0; font-size: 1rem; color: var(--text-color); outline: none; border: none; background: transparent; &::placeholder { color: var(--text-color-placeholder); opacity: 1; } } .react-aria-Button { padding: 0 6px; border-width: 0 0 0 1px; border-radius: 0 6px 6px 0; align-self: stretch; font-size: 1.5rem; &[data-focus-visible] { border-color: var(--focus-ring-color); outline-width: 1px; outline-offset: 0; } } } @import "@react-aria/example-theme"; .react-aria-Group { display: flex; align-items: center; width: fit-content; border-radius: 6px; border: 1px solid var(--border-color); background: var(--field-background); overflow: hidden; transition: all 200ms; &[data-hovered] { border-color: var(--border-color-hover); } &[data-focus-within] { outline: 2px solid var(--focus-ring-color); outline-offset: -1px; } .react-aria-Input { padding: 0.286rem; margin: 0; font-size: 1rem; color: var(--text-color); outline: none; border: none; background: transparent; &::placeholder { color: var(--text-color-placeholder); opacity: 1; } } .react-aria-Button { padding: 0 6px; border-width: 0 0 0 1px; border-radius: 0 6px 6px 0; align-self: stretch; font-size: 1.5rem; &[data-focus-visible] { border-color: var(--focus-ring-color); outline-width: 1px; outline-offset: 0; } } } @import "@react-aria/example-theme"; .react-aria-Group { display: flex; align-items: center; width: fit-content; border-radius: 6px; border: 1px solid var(--border-color); background: var(--field-background); overflow: hidden; transition: all 200ms; &[data-hovered] { border-color: var(--border-color-hover); } &[data-focus-within] { outline: 2px solid var(--focus-ring-color); outline-offset: -1px; } .react-aria-Input { padding: 0.286rem; margin: 0; font-size: 1rem; color: var(--text-color); outline: none; border: none; background: transparent; &::placeholder { color: var(--text-color-placeholder); opacity: 1; } } .react-aria-Button { padding: 0 6px; border-width: 0 0 0 1px; border-radius: 0 6px 6px 0; align-self: stretch; font-size: 1.5rem; &[data-focus-visible] { border-color: var(--focus-ring-color); outline-width: 1px; outline-offset: 0; } } } ## Features# * * * A group can be created with a `<div role="group">` or via the HTML <fieldset> element. The `Group` component supports additional UI states, and can be used standalone or as part of a larger pattern such as NumberField or DatePicker. * **Styleable** – Hover, keyboard focus, disabled, and invalid states are provided for easy styling. These states only apply when interacting with an appropriate input device, unlike CSS pseudo classes. * **Accessible** – Implemented using the ARIA "group" role by default, with optional support for the "region" landmark role. ## Anatomy# * * * A group consists of a container element for a set of semantically related UI controls. It supports states such as hover, focus within, and disabled, which are useful to style visually adjoined children. import {Group} from 'react-aria-components'; <Group> {/* ... */} </Group> import {Group} from 'react-aria-components'; <Group> {/* ... */} </Group> import {Group} from 'react-aria-components'; <Group> {/* ... */} </Group> ## Accessibility# * * * ### Labeling# Group accepts the `aria-label` and `aria-labelledby` attributes to provide an accessible label to the group as a whole. This is read by assistive technology when navigating into the group from outside. When the labels of each child element of the group do not provide sufficient context on their own, the group should receive an additional label. <span id="label-id">Serial number</span> <Group aria-labelledby="label-id"> <Input size={3} aria-label="First 3 digits" placeholder="000" /> – <Input size={2} aria-label="Middle 2 digits" placeholder="00" /> – <Input size={4} aria-label="Last 4 digits" placeholder="0000" /> </Group> <span id="label-id">Serial number</span> <Group aria-labelledby="label-id"> <Input size={3} aria-label="First 3 digits" placeholder="000" /> – <Input size={2} aria-label="Middle 2 digits" placeholder="00" /> – <Input size={4} aria-label="Last 4 digits" placeholder="0000" /> </Group> <span id="label-id"> Serial number </span> <Group aria-labelledby="label-id"> <Input size={3} aria-label="First 3 digits" placeholder="000" /> – <Input size={2} aria-label="Middle 2 digits" placeholder="00" /> – <Input size={4} aria-label="Last 4 digits" placeholder="0000" /> </Group> Serial number –– ### Role# By default, `Group` uses the group ARIA role. If the contents of the group is important enough to be included in the page table of contents, use `role="region"` instead, and ensure that an `aria-label` or `aria-labelledby` prop is assigned. <Group role="region" aria-label="Object details"> {/* ... */} </Group> <Group role="region" aria-label="Object details"> {/* ... */} </Group> <Group role="region" aria-label="Object details" > {/* ... */} </Group> If the `Group` component is used for styling purposes only, and does not include a set of related UI controls, then use `role="presentation"` instead. ## Props# * * * | Name | Type | Description | | --- | --- | --- | | `isDisabled` | `boolean` | Whether the group is disabled. | | `isInvalid` | `boolean` | Whether the group is invalid. | | `children` | `ReactNode | ( (values: GroupRenderProps & & { defaultChildren: ReactNode | | undefined } )) => ReactNode` | The children of the component. A function may be provided to alter the children based on component state. | | `className` | `string | ( (values: GroupRenderProps & & { defaultClassName: string | | undefined } )) => string` | The CSS className for the element. A function may be provided to compute the class based on component state. | | `style` | `CSSProperties | ( (values: GroupRenderProps & & { defaultStyle: CSSProperties } )) => CSSProperties | undefined` | The inline style for the element. A function may be provided to compute the style based on component state. | Events | Name | Type | Description | | --- | --- | --- | | `onHoverStart` | `( (e: HoverEvent )) => void` | Handler that is called when a hover interaction starts. | | `onHoverEnd` | `( (e: HoverEvent )) => void` | Handler that is called when a hover interaction ends. | | `onHoverChange` | `( (isHovering: boolean )) => void` | Handler that is called when the hover state changes. | Layout | Name | Type | Description | | --- | --- | --- | | `slot` | `string | null` | A slot name for the component. Slots allow the component to receive props from a parent component. An explicit `null` value indicates that the local props completely override all props received from a parent. | Accessibility | Name | Type | Default | Description | | --- | --- | --- | --- | | `role` | `'group' | 'region' | 'presentation'` | `'group'` | An accessibility role for the group. By default, this is set to `'group'`. Use `'region'` when the contents of the group is important enough to be included in the page table of contents. Use `'presentation'` if the group is visual only and does not represent a semantic grouping of controls. | | `id` | `string` | — | The element's unique identifier. See MDN. | | `aria-label` | `string` | — | Defines a string value that labels the current element. | | `aria-labelledby` | `string` | — | Identifies the element (or elements) that labels the current element. | | `aria-describedby` | `string` | — | Identifies the element (or elements) that describes the object. | | `aria-details` | `string` | — | Identifies the element (or elements) that provide a detailed, extended description for the object. | ## Styling# * * * React Aria components can be styled in many ways, including using CSS classes, inline styles, utility classes (e.g. Tailwind), CSS-in-JS (e.g. Styled Components), etc. By default, all components include a builtin `className` attribute which can be targeted using CSS selectors. These follow the `react-aria-ComponentName` naming convention. .react-aria-Group { /* ... */ } .react-aria-Group { /* ... */ } .react-aria-Group { /* ... */ } A custom `className` can also be specified on any component. This overrides the default `className` provided by React Aria with your own. <Group className="my-group"> {/* ... */} </Group> <Group className="my-group"> {/* ... */} </Group> <Group className="my-group"> {/* ... */} </Group> In addition, some components support multiple UI states (e.g. focused, placeholder, readonly, etc.). React Aria components expose states using data attributes, which you can target in CSS selectors. For example: .react-aria-Group[data-hovered] { /* ... */ } .react-aria-Group[data-focus-visible] { /* ... */ } .react-aria-Group[data-hovered] { /* ... */ } .react-aria-Group[data-focus-visible] { /* ... */ } .react-aria-Group[data-hovered] { /* ... */ } .react-aria-Group[data-focus-visible] { /* ... */ } The states, selectors, and render props for `Group` are documented below. | Name | CSS Selector | Description | | --- | --- | --- | | `isHovered` | `[data-hovered]` | Whether the group is currently hovered with a mouse. | | `isFocusWithin` | `[data-focus-within]` | Whether an element within the group is focused, either via a mouse or keyboard. | | `isFocusVisible` | `[data-focus-visible]` | Whether an element within the group is keyboard focused. | | `isDisabled` | `[data-disabled]` | Whether the group is disabled. | | `isInvalid` | `[data-invalid]` | Whether the group is invalid. | ## Advanced customization# * * * ### Contexts# All React Aria Components export a corresponding context that can be used to send props to them from a parent element. This enables you to build your own compositional APIs similar to those found in React Aria Components itself. You can send any prop or ref via context that you could pass to the corresponding component. The local props and ref on the component are merged with the ones passed via context, with the local props taking precedence (following the rules documented in mergeProps). | Component | Context | Props | Ref | | --- | --- | --- | --- | | `Group` | `GroupContext` | ` GroupProps ` | `HTMLDivElement` | This example shows a `LabeledGroup` component that accepts a label and a group as children. It uses the useId hook to generate a unique id for the label, and provides this to the group via the `aria-labelledby` prop. import {LabelContext, GroupContext} from 'react-aria-components'; import {useId} from 'react-aria'; function LabeledGroup({children}) { let labelId = useId(); return ( <LabelContext.Provider value={{id: labelId, elementType: 'span'}}> <GroupContext.Provider value={{'aria-labelledby': labelId}}> {children} </GroupContext.Provider> </LabelContext.Provider> ); } <LabeledGroup> <Label>Expiration date</Label> <Group> <Input size={3} aria-label="Month" placeholder="mm" /> / <Input size={4} aria-label="Year" placeholder="yyyy" /> </Group> </LabeledGroup> import { GroupContext, LabelContext } from 'react-aria-components'; import {useId} from 'react-aria'; function LabeledGroup({ children }) { let labelId = useId(); return ( <LabelContext.Provider value={{ id: labelId, elementType: 'span' }} > <GroupContext.Provider value={{ 'aria-labelledby': labelId }} > {children} </GroupContext.Provider> </LabelContext.Provider> ); } <LabeledGroup> <Label>Expiration date</Label> <Group> <Input size={3} aria-label="Month" placeholder="mm" /> / <Input size={4} aria-label="Year" placeholder="yyyy" /> </Group> </LabeledGroup> import { GroupContext, LabelContext } from 'react-aria-components'; import {useId} from 'react-aria'; function LabeledGroup( { children } ) { let labelId = useId(); return ( <LabelContext.Provider value={{ id: labelId, elementType: 'span' }} > <GroupContext.Provider value={{ 'aria-labelledby': labelId }} > {children} </GroupContext.Provider> </LabelContext.Provider> ); } <LabeledGroup> <Label> Expiration date </Label> <Group> <Input size={3} aria-label="Month" placeholder="mm" /> / <Input size={4} aria-label="Year" placeholder="yyyy" /> </Group> </LabeledGroup> Expiration date / --- ## Page: https://react-spectrum.adobe.com/react-aria/Toolbar.html # Toolbar A toolbar is a container for a set of interactive controls, such as buttons, dropdown menus, or checkboxes, with arrow key navigation. <table><tbody><tr><th>install</th><td><code>yarn add react-aria-components</code></td></tr><tr><th>version</th><td>1.10.1</td></tr><tr><th>usage</th><td><code><span>import</span> {Toolbar} <span>from</span> <span>'react-aria-components'</span></code></td></tr></tbody></table> View ARIA pattern W3C View repository GitHub View package NPM ## Example# * * * import {Button, Checkbox, Group, Separator, ToggleButton, Toolbar} from 'react-aria-components'; <Toolbar aria-label="Text formatting"> <Group aria-label="Style"> <ToggleButton aria-label="Bold"> <b>B</b> </ToggleButton> <ToggleButton aria-label="Italic"> <i>I</i> </ToggleButton> <ToggleButton aria-label="Underline"> <u>U</u> </ToggleButton> </Group> <Separator orientation="vertical" /> <Group aria-label="Clipboard"> <Button>Copy</Button> <Button>Paste</Button> <Button>Cut</Button> </Group> <Separator orientation="vertical" /> <Checkbox> <div className="checkbox"> <svg viewBox="0 0 18 18" aria-hidden="true"> <polyline points="1 9 7 14 15 4" /> </svg> </div> Night Mode </Checkbox> </Toolbar> import { Button, Checkbox, Group, Separator, ToggleButton, Toolbar } from 'react-aria-components'; <Toolbar aria-label="Text formatting"> <Group aria-label="Style"> <ToggleButton aria-label="Bold"> <b>B</b> </ToggleButton> <ToggleButton aria-label="Italic"> <i>I</i> </ToggleButton> <ToggleButton aria-label="Underline"> <u>U</u> </ToggleButton> </Group> <Separator orientation="vertical" /> <Group aria-label="Clipboard"> <Button>Copy</Button> <Button>Paste</Button> <Button>Cut</Button> </Group> <Separator orientation="vertical" /> <Checkbox> <div className="checkbox"> <svg viewBox="0 0 18 18" aria-hidden="true"> <polyline points="1 9 7 14 15 4" /> </svg> </div> Night Mode </Checkbox> </Toolbar> import { Button, Checkbox, Group, Separator, ToggleButton, Toolbar } from 'react-aria-components'; <Toolbar aria-label="Text formatting"> <Group aria-label="Style"> <ToggleButton aria-label="Bold"> <b>B</b> </ToggleButton> <ToggleButton aria-label="Italic"> <i>I</i> </ToggleButton> <ToggleButton aria-label="Underline"> <u>U</u> </ToggleButton> </Group> <Separator orientation="vertical" /> <Group aria-label="Clipboard"> <Button> Copy </Button> <Button> Paste </Button> <Button> Cut </Button> </Group> <Separator orientation="vertical" /> <Checkbox> <div className="checkbox"> <svg viewBox="0 0 18 18" aria-hidden="true" > <polyline points="1 9 7 14 15 4" /> </svg> </div> Night Mode </Checkbox> </Toolbar> **B**_I_U CopyPasteCut Night Mode Show CSS @import "@react-aria/example-theme"; .react-aria-Toolbar { display: flex; flex-wrap: wrap; gap: 5px; &[data-orientation=horizontal] { flex-direction: row; } .react-aria-Group { display: contents; } .react-aria-ToggleButton { width: 32px; } } .react-aria-Separator { align-self: stretch; background-color: var(--border-color); &[aria-orientation=vertical] { width: 1px; margin: 0px 10px; } } @import "@react-aria/example-theme"; .react-aria-Toolbar { display: flex; flex-wrap: wrap; gap: 5px; &[data-orientation=horizontal] { flex-direction: row; } .react-aria-Group { display: contents; } .react-aria-ToggleButton { width: 32px; } } .react-aria-Separator { align-self: stretch; background-color: var(--border-color); &[aria-orientation=vertical] { width: 1px; margin: 0px 10px; } } @import "@react-aria/example-theme"; .react-aria-Toolbar { display: flex; flex-wrap: wrap; gap: 5px; &[data-orientation=horizontal] { flex-direction: row; } .react-aria-Group { display: contents; } .react-aria-ToggleButton { width: 32px; } } .react-aria-Separator { align-self: stretch; background-color: var(--border-color); &[aria-orientation=vertical] { width: 1px; margin: 0px 10px; } } ## Features# * * * There is no built-in HTML toolbar element. `Toolbar` helps achieve accessible toolbars with arrow key navigation that can be styled as needed. * **Flexible** – Support for interactive children such as buttons, checkboxes, dropdown menus, etc. in a horizontal or vertical orientation. * **Accessible** – Follows the ARIA toolbar pattern with support for arrow key navigation as a single tab stop. ## Anatomy# * * * A toolbar consists of a container element for a set of interactive controls. It provides arrow key navigation between its children, in either horizontal or vertical orientation. import {Toolbar} from 'react-aria-components'; <Toolbar> {/* ... */} </Toolbar> import {Toolbar} from 'react-aria-components'; <Toolbar> {/* ... */} </Toolbar> import {Toolbar} from 'react-aria-components'; <Toolbar> {/* ... */} </Toolbar> ## Starter kits# * * * To help kick-start your project, we offer starter kits that include example implementations of all React Aria components with various styling solutions. All components are fully styled, including support for dark mode, high contrast mode, and all UI states. Each starter comes with a pre-configured Storybook that you can experiment with, or use as a starting point for your own component library. Vanilla CSS Download ZIP Preview Tailwind CSS Download ZIP Preview ## Orientation# * * * By default, toolbars are horizontally oriented. The `orientation` prop can be set to `"vertical"` to change the arrow key navigation behavior. import Move from '@spectrum-icons/workflow/Move'; import Select from '@spectrum-icons/workflow/Select'; import Polygon from '@spectrum-icons/workflow/PolygonSelect'; import Brush from '@spectrum-icons/workflow/Brush'; import Pencil from '@spectrum-icons/workflow/Edit'; <Toolbar aria-label="Tools" orientation="vertical"> <Group aria-label="Select"> <Button aria-label="Move"><Move size="S" /></Button> <Button aria-label="Rectangle"><Select size="S" /></Button> <Button aria-label="Polygon"><Polygon size="S" /></Button> </Group> <Separator orientation="horizontal" /> <Group aria-label="Draw"> <Button aria-label="Brush"><Brush size="S" /></Button> <Button aria-label="Pencil"><Pencil size="S" /></Button> </Group> </Toolbar> import Move from '@spectrum-icons/workflow/Move'; import Select from '@spectrum-icons/workflow/Select'; import Polygon from '@spectrum-icons/workflow/PolygonSelect'; import Brush from '@spectrum-icons/workflow/Brush'; import Pencil from '@spectrum-icons/workflow/Edit'; <Toolbar aria-label="Tools" orientation="vertical"> <Group aria-label="Select"> <Button aria-label="Move"> <Move size="S" /> </Button> <Button aria-label="Rectangle"> <Select size="S" /> </Button> <Button aria-label="Polygon"> <Polygon size="S" /> </Button> </Group> <Separator orientation="horizontal" /> <Group aria-label="Draw"> <Button aria-label="Brush"> <Brush size="S" /> </Button> <Button aria-label="Pencil"> <Pencil size="S" /> </Button> </Group> </Toolbar> import Move from '@spectrum-icons/workflow/Move'; import Select from '@spectrum-icons/workflow/Select'; import Polygon from '@spectrum-icons/workflow/PolygonSelect'; import Brush from '@spectrum-icons/workflow/Brush'; import Pencil from '@spectrum-icons/workflow/Edit'; <Toolbar aria-label="Tools" orientation="vertical" > <Group aria-label="Select"> <Button aria-label="Move"> <Move size="S" /> </Button> <Button aria-label="Rectangle"> <Select size="S" /> </Button> <Button aria-label="Polygon"> <Polygon size="S" /> </Button> </Group> <Separator orientation="horizontal" /> <Group aria-label="Draw"> <Button aria-label="Brush"> <Brush size="S" /> </Button> <Button aria-label="Pencil"> <Pencil size="S" /> </Button> </Group> </Toolbar> * * * Show CSS .react-aria-Toolbar { width: fit-content; &[data-orientation=vertical] { flex-direction: column; align-items: start; } } .react-aria-Separator { &:not([aria-orientation=vertical]) { border: none; height: 1px; width: 100%; margin: 10px 0; } } .react-aria-Toolbar { width: fit-content; &[data-orientation=vertical] { flex-direction: column; align-items: start; } } .react-aria-Separator { &:not([aria-orientation=vertical]) { border: none; height: 1px; width: 100%; margin: 10px 0; } } .react-aria-Toolbar { width: fit-content; &[data-orientation=vertical] { flex-direction: column; align-items: start; } } .react-aria-Separator { &:not([aria-orientation=vertical]) { border: none; height: 1px; width: 100%; margin: 10px 0; } } ## Props# * * * ### Toolbar# | Name | Type | Default | Description | | --- | --- | --- | --- | | `orientation` | ` Orientation ` | `'horizontal'` | The orientation of the entire toolbar. | | `children` | `ReactNode | ( (values: ToolbarRenderProps & & { defaultChildren: ReactNode | | undefined } )) => ReactNode` | — | The children of the component. A function may be provided to alter the children based on component state. | | `className` | `string | ( (values: ToolbarRenderProps & & { defaultClassName: string | | undefined } )) => string` | — | The CSS className for the element. A function may be provided to compute the class based on component state. | | `style` | `CSSProperties | ( (values: ToolbarRenderProps & & { defaultStyle: CSSProperties } )) => CSSProperties | undefined` | — | The inline style for the element. A function may be provided to compute the style based on component state. | Layout | Name | Type | Description | | --- | --- | --- | | `slot` | `string | null` | A slot name for the component. Slots allow the component to receive props from a parent component. An explicit `null` value indicates that the local props completely override all props received from a parent. | Accessibility | Name | Type | Description | | --- | --- | --- | | `aria-label` | `string` | Defines a string value that labels the current element. | | `aria-labelledby` | `string` | Identifies the element (or elements) that labels the current element. | | `aria-describedby` | `string` | Identifies the element (or elements) that describes the object. | | `aria-details` | `string` | Identifies the element (or elements) that provide a detailed, extended description for the object. | ### Separator# A `<Separator>` can be placed between elements and groups in a toolbar. Show props | Name | Type | Default | Description | | --- | --- | --- | --- | | `orientation` | ` Orientation ` | `'horizontal'` | The orientation of the separator. | | `elementType` | `string` | — | The HTML element type that will be used to render the separator. | | `className` | `string` | — | The CSS className for the element. | | `style` | `CSSProperties` | — | The inline style for the element. | Layout | Name | Type | Description | | --- | --- | --- | | `slot` | `string | null` | A slot name for the component. Slots allow the component to receive props from a parent component. An explicit `null` value indicates that the local props completely override all props received from a parent. | Accessibility | Name | Type | Description | | --- | --- | --- | | `id` | `string` | The element's unique identifier. See MDN. | | `aria-label` | `string` | Defines a string value that labels the current element. | | `aria-labelledby` | `string` | Identifies the element (or elements) that labels the current element. | | `aria-describedby` | `string` | Identifies the element (or elements) that describes the object. | | `aria-details` | `string` | Identifies the element (or elements) that provide a detailed, extended description for the object. | ## Styling# * * * React Aria components can be styled in many ways, including using CSS classes, inline styles, utility classes (e.g. Tailwind), CSS-in-JS (e.g. Styled Components), etc. By default, all components include a builtin `className` attribute which can be targeted using CSS selectors. These follow the `react-aria-ComponentName` naming convention. .react-aria-Toolbar { /* ... */ } .react-aria-Toolbar { /* ... */ } .react-aria-Toolbar { /* ... */ } A custom `className` can also be specified on any component. This overrides the default `className` provided by React Aria with your own. <Toolbar className="my-toolbar"> {/* ... */} </Toolbar> <Toolbar className="my-toolbar"> {/* ... */} </Toolbar> <Toolbar className="my-toolbar"> {/* ... */} </Toolbar> The `className` and `style` props also accept functions which receive states for styling. This lets you dynamically determine the classes or styles to apply, which is useful when using utility CSS libraries like Tailwind. <Toolbar className={({ orientation }) => orientation === 'vertical' ? 'flex-col' : 'flex-row'} > {/* ... */} </Toolbar> <Toolbar className={({ orientation }) => orientation === 'vertical' ? 'flex-col' : 'flex-row'} > {/* ... */} </Toolbar> <Toolbar className={( { orientation } ) => orientation === 'vertical' ? 'flex-col' : 'flex-row'} > {/* ... */} </Toolbar> The selectors and render props for each component used in a `Toolbar` are documented below. ### Toolbar# A `Toolbar` can be targeted with the `.react-aria-Toolbar` CSS selector, or by overriding with a custom `className`. It supports the following states and render props: | Name | CSS Selector | Description | | --- | --- | --- | | `orientation` | `[data-orientation]` | The current orientation of the toolbar. | ### Separator# A `Separator` can be targeted with the `.react-aria-Separator` CSS selector, or by overriding with a custom `className`. ## Advanced customization# * * * ### Contexts# All React Aria Components export a corresponding context that can be used to send props to them from a parent element. This enables you to build your own compositional APIs similar to those found in React Aria Components itself. You can send any prop or ref via context that you could pass to the corresponding component. The local props and ref on the component are merged with the ones passed via context, with the local props taking precedence (following the rules documented in mergeProps). | Component | Context | Props | Ref | | --- | --- | --- | --- | | `Toolbar` | `ToolbarContext` | ` ToolbarProps ` | `HTMLDivElement` | ### Hooks# If you need to customize things further, such as customizing the DOM structure, you can drop down to the lower level Hook-based API. See useToolbar for more details. --- ## Page: https://react-spectrum.adobe.com/react-aria/hooks.html # React Aria Hooks This page describes how to get started with React Aria hooks. ## Installation# * * * React Aria can be installed using a package manager like npm or yarn. yarn add react-aria If you prefer, you can also use our hooks from individually versioned packages. This allows you to only install the hooks you use, or more granularly manage their versions. The individual packages are published under the @react-aria scope on npm. For example: yarn add @react-aria/button Once installed, hooks can be used from the monopackage or individual packages the same way. // Monopackage import {useButton} from 'react-aria'; // Monopackage import {useButton} from 'react-aria'; // Monopackage import {useButton} from 'react-aria'; // Individual packages import {useButton} from '@react-aria/button'; // Individual packages import {useButton} from '@react-aria/button'; // Individual packages import {useButton} from '@react-aria/button'; ## Building a component# * * * React Aria provides behavior and accessibility through React Hooks. Since it does not provide any rendering, you are responsible for defining the DOM structure for your component and passing the DOM props returned by each React Aria hook to the appropriate elements. This is powerful because it allows you to be in complete control over the DOM structure that you render. For example, you may need to add extra elements for styling or layout control. You also get complete control over how you style your components: you could use CSS classes, inline styles, CSS-in-JS, etc. Start by importing the hook you wish to use, and calling it in your component. You'll typically pass through the props from your component, along with a ref to the DOM node in some cases. The hook will return one or more sets of DOM props which you should pass through to the appropriate element. This can be done by spreading the props returned from the hook onto the element that you render. This example shows a very simple button component built with the useButton hook. import {useButton} from 'react-aria'; function Button(props) { let ref = React.useRef(null); let { buttonProps, isPressed } = useButton(props, ref); return ( <button ref={ref} {...buttonProps} style={{ background: isPressed ? '#444' : '#666', color: 'white', padding: '6px 12px', borderRadius: 4, border: 'none' }} > {props.children} </button> ); } <Button onPress={() => alert('Button pressed!')}> Press me </Button> import {useButton} from 'react-aria'; function Button(props) { let ref = React.useRef(null); let { buttonProps, isPressed } = useButton(props, ref); return ( <button ref={ref} {...buttonProps} style={{ background: isPressed ? '#444' : '#666', color: 'white', padding: '6px 12px', borderRadius: 4, border: 'none' }} > {props.children} </button> ); } <Button onPress={() => alert('Button pressed!')}> Press me </Button> import {useButton} from 'react-aria'; function Button(props) { let ref = React.useRef( null ); let { buttonProps, isPressed } = useButton( props, ref ); return ( <button ref={ref} {...buttonProps} style={{ background: isPressed ? '#444' : '#666', color: 'white', padding: '6px 12px', borderRadius: 4, border: 'none' }} > {props.children} </button> ); } <Button onPress={() => alert( 'Button pressed!' )} > Press me </Button> Press me Now you just need to add your own styling, and you have a fully accessible button component that works consistently across mouse, touch, keyboard, and screen readers with high quality interactions! See the useButton docs more examples. ## Stateful components# * * * Many components are **stateless** — they display information to a user. Examples of stateless components include buttons, progress bars, and links. These components don't update as the user interacts with them. A **stateful** component has some state, and allows a user to interact with the component in order to update that state. Examples of stateful components include text fields, checkboxes, and selects. React Aria separates the state management logic into a separate hook that lives in react-stately. The state hook holds the state for the component, and provides an interface to read and update that state. This allows this logic to be reused across different platforms, e.g. in react-native. Read more about this on the architecture page. To build a stateful component, you'll need to install and import the corresponding state hook from react-stately. Then, call the state hook from your component, and pass the resulting state object to the React Aria hook. You can also use the state in your rendering code to determine what visual state to display. This example shows a number field component built with the useNumberField hook, along with the useNumberFieldState hook from react-stately. import {useNumberFieldState} from 'react-stately'; import {useLocale, useNumberField} from 'react-aria'; function NumberField(props) { let {locale} = useLocale(); let state = useNumberFieldState({...props, locale}); let inputRef = React.useRef(null); let { labelProps, groupProps, inputProps, incrementButtonProps, decrementButtonProps } = useNumberField(props, state, inputRef); return ( <div> <label {...labelProps}>{props.label}</label> <div {...groupProps} style={{display: 'flex', gap: 4}}> <Button {...decrementButtonProps}>-</Button> <input {...inputProps} ref={inputRef} /> <Button {...incrementButtonProps}>+</Button> </div> </div> ); } <NumberField label="Price" defaultValue={6} formatOptions={{ style: 'currency', currency: 'USD' }} /> import {useNumberFieldState} from 'react-stately'; import {useLocale, useNumberField} from 'react-aria'; function NumberField(props) { let { locale } = useLocale(); let state = useNumberFieldState({ ...props, locale }); let inputRef = React.useRef(null); let { labelProps, groupProps, inputProps, incrementButtonProps, decrementButtonProps } = useNumberField(props, state, inputRef); return ( <div> <label {...labelProps}>{props.label}</label> <div {...groupProps} style={{ display: 'flex', gap: 4 }} > <Button {...decrementButtonProps}>-</Button> <input {...inputProps} ref={inputRef} /> <Button {...incrementButtonProps}>+</Button> </div> </div> ); } <NumberField label="Price" defaultValue={6} formatOptions={{ style: 'currency', currency: 'USD' }} /> import {useNumberFieldState} from 'react-stately'; import { useLocale, useNumberField } from 'react-aria'; function NumberField( props ) { let { locale } = useLocale(); let state = useNumberFieldState({ ...props, locale }); let inputRef = React .useRef(null); let { labelProps, groupProps, inputProps, incrementButtonProps, decrementButtonProps } = useNumberField( props, state, inputRef ); return ( <div> <label {...labelProps} > {props.label} </label> <div {...groupProps} style={{ display: 'flex', gap: 4 }} > <Button {...decrementButtonProps} > - </Button> <input {...inputProps} ref={inputRef} /> <Button {...incrementButtonProps} > + </Button> </div> </div> ); } <NumberField label="Price" defaultValue={6} formatOptions={{ style: 'currency', currency: 'USD' }} /> Price \-+ This gives you a number field complete with input validation, internationalized formatting, accessibility, mobile keyboard support, and much more! See the useNumberField docs to learn more about all of the features. --- ## Page: https://react-spectrum.adobe.com/react-aria/useButton.html # useButton Provides the behavior and accessibility implementation for a button component. Handles mouse, keyboard, and touch interactions, focus behavior, and ARIA props for both native button elements and custom element types. <table><tbody><tr><th>install</th><td><code>yarn add react-aria</code></td></tr><tr><th>version</th><td>3.41.1</td></tr><tr><th>usage</th><td><code><span>import</span> {useButton} <span>from</span> <span>'react-aria'</span></code></td></tr></tbody></table> View ARIA pattern W3C View repository GitHub View package NPM ## API# * * * `useButton( (props: AriaButtonOptions <ElementType>, , ref: RefObject<any> )): ButtonAria <HTMLAttributes<any>>` ## Features# * * * On the surface, building a custom styled button seems simple. However, there are many cross browser inconsistencies in interactions and accessibility features to consider. `useButton` handles all of these interactions for you, so you can focus on the styling. * Native HTML `<button>` support * `<a>` and custom element type support via ARIA * Mouse and touch event handling, and press state management * Keyboard focus management and cross browser normalization * Keyboard event support for Space and Enter keys Read our blog post about the complexities of building buttons that work well across devices and interaction methods. ## Anatomy# * * * Buttons consist of a clickable area usually containing a textual label or an icon that users can click to perform an action. In addition, keyboard users may activate buttons using the Space or Enter keys. If a visual label is not provided (e.g. an icon only button), then an `aria-label` or `aria-labelledby` prop must be passed to identify the button to assistive technology. ## Example# * * * By default, `useButton` assumes that you are using it with a native `<button>` element. import {useButton} from 'react-aria'; import {useRef} from 'react'; function Button(props) { let ref = useRef<HTMLButtonElement | null>(null); let { buttonProps } = useButton(props, ref); let { children } = props; return ( <button {...buttonProps} ref={ref}> {children} </button> ); } <Button onPress={() => alert('Button pressed!')}>Test</Button> import {useButton} from 'react-aria'; import {useRef} from 'react'; function Button(props) { let ref = useRef<HTMLButtonElement | null>(null); let { buttonProps } = useButton(props, ref); let { children } = props; return ( <button {...buttonProps} ref={ref}> {children} </button> ); } <Button onPress={() => alert('Button pressed!')}> Test </Button> import {useButton} from 'react-aria'; import {useRef} from 'react'; function Button(props) { let ref = useRef< | HTMLButtonElement | null >(null); let { buttonProps } = useButton( props, ref ); let { children } = props; return ( <button {...buttonProps} ref={ref} > {children} </button> ); } <Button onPress={() => alert( 'Button pressed!' )} > Test </Button> Test ## Custom element type# * * * Sometimes you might need to use an element other than a native `<button>`. `useButton` supports this via the `elementType` prop. When used with an element other than a native button, `useButton` automatically applies the necessary ARIA roles and attributes to ensure that the element is exposed to assistive technology as a button. In addition, this example shows usage of the `isPressed` value returned by `useButton` to properly style the button's active state. You could use the CSS `:active` pseudo class for this, but `isPressed` properly handles when the user drags their pointer off of the button, along with keyboard support and better touch screen support. function Button(props) { let { children } = props; let ref = useRef<HTMLButtonElement | null>(null); let { buttonProps, isPressed } = useButton({ ...props, elementType: 'span' }, ref); return ( <span {...buttonProps} style={{ background: isPressed ? 'darkgreen' : 'green', color: 'white', padding: 10, cursor: 'pointer', userSelect: 'none', WebkitUserSelect: 'none' }} ref={ref} > {children} </span> ); } <Button onPress={() => alert('Button pressed!')}>Test</Button> function Button(props) { let { children } = props; let ref = useRef<HTMLButtonElement | null>(null); let { buttonProps, isPressed } = useButton({ ...props, elementType: 'span' }, ref); return ( <span {...buttonProps} style={{ background: isPressed ? 'darkgreen' : 'green', color: 'white', padding: 10, cursor: 'pointer', userSelect: 'none', WebkitUserSelect: 'none' }} ref={ref} > {children} </span> ); } <Button onPress={() => alert('Button pressed!')}> Test </Button> function Button(props) { let { children } = props; let ref = useRef< | HTMLButtonElement | null >(null); let { buttonProps, isPressed } = useButton({ ...props, elementType: 'span' }, ref); return ( <span {...buttonProps} style={{ background: isPressed ? 'darkgreen' : 'green', color: 'white', padding: 10, cursor: 'pointer', userSelect: 'none', WebkitUserSelect: 'none' }} ref={ref} > {children} </span> ); } <Button onPress={() => alert( 'Button pressed!' )} > Test </Button> Test ## Usage# * * * The following examples show how to use the `Button` component created in the above example. ### Events# `useButton` supports user interactions via mouse, keyboard, and touch. You can handle all of these via the `onPress` prop. This is similar to the standard `onClick` event, but normalized to support all interaction methods equally. In addition, the `onPressStart`, `onPressEnd`, and `onPressChange` events are fired as the user interacts with the button. Each of these handlers receives a `PressEvent` , which exposes information about the target and the type of event that triggered the interaction. See usePress for more details. function Example() { let [pointerType, setPointerType] = React.useState(null); return ( <> <Button onPressStart={(e) => setPointerType(e.pointerType)} onPressEnd={(e) => setPointerType(null)} > Press me </Button> <p> {pointerType ? `You are pressing the button with a ${pointerType}!` : 'Ready to be pressed.'} </p> </> ); } function Example() { let [pointerType, setPointerType] = React.useState(null); return ( <> <Button onPressStart={(e) => setPointerType(e.pointerType)} onPressEnd={(e) => setPointerType(null)} > Press me </Button> <p> {pointerType ? `You are pressing the button with a ${pointerType}!` : 'Ready to be pressed.'} </p> </> ); } function Example() { let [ pointerType, setPointerType ] = React.useState( null ); return ( <> <Button onPressStart={(e) => setPointerType( e.pointerType )} onPressEnd={(e) => setPointerType( null )} > Press me </Button> <p> {pointerType ? `You are pressing the button with a ${pointerType}!` : 'Ready to be pressed.'} </p> </> ); } Press me Ready to be pressed. ### Disabled# A `Button` can be disabled using the `isDisabled` prop. <Button isDisabled>Pin</Button> <Button isDisabled>Pin</Button> <Button isDisabled> Pin </Button> Pin --- ## Page: https://react-spectrum.adobe.com/react-aria/useToggleButton.html # useToggleButton Provides the behavior and accessibility implementation for a toggle button component. ToggleButtons allow users to toggle a selection on or off, for example switching between two states or modes. <table><tbody><tr><th>install</th><td><code>yarn add react-aria</code></td></tr><tr><th>version</th><td>3.41.1</td></tr><tr><th>usage</th><td><code><span>import</span> {useToggleButton} <span>from</span> <span>'react-aria'</span></code></td></tr></tbody></table> View ARIA pattern W3C View repository GitHub View package NPM ## API# * * * `useToggleButton( props: AriaToggleButtonOptions <ElementType>, state: ToggleState , ref: RefObject<any> ): ToggleButtonAria <HTMLAttributes<any>>` ## Features# * * * Toggle buttons are similar to action buttons, but support an additional selection state that is toggled when a user presses the button. There is no built-in HTML element that represents a toggle button, so React Aria implements it using ARIA attributes. * Native HTML `<button>`, `<a>`, and custom element type support * Exposed as a toggle button via ARIA * Mouse and touch event handling, and press state management * Keyboard focus management and cross browser normalization * Keyboard event support for Space and Enter keys ## Anatomy# * * * Toggle buttons consist of a clickable area usually containing a textual label or an icon that users can click to toggle a selection state. In addition, keyboard users may toggle the state using the Space or Enter keys. `useToggleButton` returns props to be spread onto the button element, along with a boolean indicating whether the user is currently pressing the button: | Name | Type | Description | | --- | --- | --- | | `isSelected` | `boolean` | Whether the button is selected. | | `isDisabled` | `boolean` | Whether the button is disabled. | | `buttonProps` | `T` | Props for the button element. | | `isPressed` | `boolean` | Whether the button is currently pressed. | Selection state is managed by the `useToggleState` hook in `@react-stately/toggle`. The state object should be passed as an option to `useToggleButton`. If a visual label is not provided (e.g. an icon only button), then an `aria-label` or `aria-labelledby` prop must be passed to identify the button to assistive technology. ## Example# * * * By default, `useToggleButton` assumes that you are using it with a native `<button>` element. You can use a custom element type by passing the `elementType` prop to `useToggleButton`. See the useButton docs for an example of this. The following example shows how to use the `useToggleButton` and `useToggleState` hooks to build a toggle button. The toggle state is used to switch between a green and blue background when unselected and selected respectively. In addition, the `isPressed` state is used to adjust the background to be darker when the user presses down on the button. import {useToggleState} from 'react-stately'; import {useToggleButton} from 'react-aria'; import {useRef} from 'react'; function ToggleButton(props) { let ref = useRef<HTMLButtonElement | null>(null); let state = useToggleState(props); let { buttonProps, isPressed } = useToggleButton(props, state, ref); return ( <button {...buttonProps} style={{ background: isPressed ? state.isSelected ? 'darkgreen' : 'gray' : state.isSelected ? 'green' : 'lightgray', color: state.isSelected ? 'white' : 'black', padding: 10, fontSize: 16, userSelect: 'none', WebkitUserSelect: 'none', border: 'none' }} ref={ref} > {props.children} </button> ); } <ToggleButton>Pin</ToggleButton> import {useToggleState} from 'react-stately'; import {useToggleButton} from 'react-aria'; import {useRef} from 'react'; function ToggleButton(props) { let ref = useRef<HTMLButtonElement | null>(null); let state = useToggleState(props); let { buttonProps, isPressed } = useToggleButton( props, state, ref ); return ( <button {...buttonProps} style={{ background: isPressed ? state.isSelected ? 'darkgreen' : 'gray' : state.isSelected ? 'green' : 'lightgray', color: state.isSelected ? 'white' : 'black', padding: 10, fontSize: 16, userSelect: 'none', WebkitUserSelect: 'none', border: 'none' }} ref={ref} > {props.children} </button> ); } <ToggleButton>Pin</ToggleButton> import {useToggleState} from 'react-stately'; import {useToggleButton} from 'react-aria'; import {useRef} from 'react'; function ToggleButton( props ) { let ref = useRef< | HTMLButtonElement | null >(null); let state = useToggleState( props ); let { buttonProps, isPressed } = useToggleButton( props, state, ref ); return ( <button {...buttonProps} style={{ background: isPressed ? state .isSelected ? 'darkgreen' : 'gray' : state .isSelected ? 'green' : 'lightgray', color: state .isSelected ? 'white' : 'black', padding: 10, fontSize: 16, userSelect: 'none', WebkitUserSelect: 'none', border: 'none' }} ref={ref} > {props.children} </button> ); } <ToggleButton> Pin </ToggleButton> Pin ## Usage# * * * The following examples show how to use the `ToggleButton` component created in the above example. ### Controlled selection state# A default selection state for a toggle button can be set using the `defaultSelected` prop, or controlled with the `isSelected` prop. The `onChange` event is fired when the user presses the button, toggling the boolean. See React's documentation on uncontrolled components for more info. function Example() { let [isSelected, setSelected] = React.useState(false); return ( <ToggleButton isSelected={isSelected} onChange={setSelected} aria-label="Star"> ★ </ToggleButton> ); } function Example() { let [isSelected, setSelected] = React.useState(false); return ( <ToggleButton isSelected={isSelected} onChange={setSelected} aria-label="Star"> ★ </ToggleButton> ); } function Example() { let [ isSelected, setSelected ] = React.useState( false ); return ( <ToggleButton isSelected={isSelected} onChange={setSelected} aria-label="Star" > ★ </ToggleButton> ); } ★ ### Disabled# A `ToggleButton` can be disabled using the `isDisabled` prop. <ToggleButton isDisabled>Pin</ToggleButton> <ToggleButton isDisabled>Pin</ToggleButton> <ToggleButton isDisabled > Pin </ToggleButton> Pin --- ## Page: https://react-spectrum.adobe.com/react-aria/useToggleButtonGroup.html # useToggleButtonGroup <table><tbody><tr><th>install</th><td><code>yarn add react-aria</code></td></tr><tr><th>version</th><td>3.41.1</td></tr><tr><th>usage</th><td><code><span>import</span> {useToggleButtonGroup} <span>from</span> <span>'react-aria'</span></code></td></tr></tbody></table> View ARIA pattern W3C View repository GitHub View package NPM ## API# * * * `useToggleButtonGroup( props: AriaToggleButtonGroupProps , state: ToggleGroupState , ref: RefObject <HTMLElement | | null> ): ToggleButtonGroupAria ``useToggleButtonGroupItem( props: AriaToggleButtonGroupItemOptions <ElementType>, state: ToggleGroupState , ref: RefObject <any> ): ToggleButtonAria <HTMLAttributes<any>>` ## Features# * * * There is no built in element for toggle button groups in HTML. `useToggleButtonGroup` helps achieve accessible toggle button groups that can be styled as needed. * **Accessible** – Represented as an ARIA radiogroup when using single selection, or a toolbar when using multiple selection. * **Keyboard navigation** – Users can navigate between buttons with the arrow keys. Selection can be toggled using the Enter or Space keys. * **Styleable** – Hover, press, keyboard focus, and selection states are provided for easy styling. ## Anatomy# * * * A toggle button group consists of a set of toggle buttons, and coordinates the selection state between them. Users can navigate between buttons with the arrow keys in either horizontal or vertical orientations. `useToggleButtonGroup` returns props for the toggle button group: | Name | Type | Description | | --- | --- | --- | | `groupProps` | `DOMAttributes` | Props for the toggle button group container. | `useToggleButtonGroupItem` returns props for an individual toggle button: | Name | Type | Description | | --- | --- | --- | | `isSelected` | `boolean` | Whether the button is selected. | | `isDisabled` | `boolean` | Whether the button is disabled. | | `buttonProps` | `T` | Props for the button element. | | `isPressed` | `boolean` | Whether the button is currently pressed. | Selection state is managed by the `useToggleGroupState` hook in `@react-stately/toggle`. The state object should be passed as an option to `useToggleButtonGroup` and `useToggleButtonGroupItem`. **Note:** `useToggleButtonGroupItem` should only be used when it is contained within a toggle button group. For a standalone toggle button, use the useToggleButton hook instead. ## Example# * * * import type {ToggleGroupState} from 'react-stately'; import type {AriaToggleButtonGroupItemProps, AriaToggleButtonGroupProps} from 'react-aria'; import {useToggleGroupState} from 'react-stately'; import {useToggleButtonGroup, useToggleButtonGroupItem} from 'react-aria'; interface ToggleButtonGroupProps extends AriaToggleButtonGroupProps { children: React.ReactNode; } let ToggleButtonGroupContext = React.createContext<ToggleGroupState | null>( null ); function ToggleButtonGroup(props: ToggleButtonGroupProps) { let state = useToggleGroupState(props); let ref = React.useRef<HTMLDivElement | null>(null); let { groupProps } = useToggleButtonGroup(props, state, ref); return ( <div {...groupProps} className="toggle-group" ref={ref}> <ToggleButtonGroupContext.Provider value={state}> {props.children} </ToggleButtonGroupContext.Provider> </div> ); } function ToggleButton(props: AriaToggleButtonGroupItemProps) { let ref = React.useRef<HTMLButtonElement | null>(null); let state = React.useContext(ToggleButtonGroupContext)!; let { buttonProps, isPressed, isSelected } = useToggleButtonGroupItem( props, state, ref ); return ( <button {...buttonProps} className="toggle-button" data-pressed={isPressed} data-selected={isSelected} ref={ref} > {props.children} </button> ); } <ToggleButtonGroup> <ToggleButton id="left">Left</ToggleButton> <ToggleButton id="center">Center</ToggleButton> <ToggleButton id="right">Right</ToggleButton> </ToggleButtonGroup> import type {ToggleGroupState} from 'react-stately'; import type { AriaToggleButtonGroupItemProps, AriaToggleButtonGroupProps } from 'react-aria'; import {useToggleGroupState} from 'react-stately'; import { useToggleButtonGroup, useToggleButtonGroupItem } from 'react-aria'; interface ToggleButtonGroupProps extends AriaToggleButtonGroupProps { children: React.ReactNode; } let ToggleButtonGroupContext = React.createContext< ToggleGroupState | null >(null); function ToggleButtonGroup(props: ToggleButtonGroupProps) { let state = useToggleGroupState(props); let ref = React.useRef<HTMLDivElement | null>(null); let { groupProps } = useToggleButtonGroup( props, state, ref ); return ( <div {...groupProps} className="toggle-group" ref={ref}> <ToggleButtonGroupContext.Provider value={state}> {props.children} </ToggleButtonGroupContext.Provider> </div> ); } function ToggleButton( props: AriaToggleButtonGroupItemProps ) { let ref = React.useRef<HTMLButtonElement | null>(null); let state = React.useContext(ToggleButtonGroupContext)!; let { buttonProps, isPressed, isSelected } = useToggleButtonGroupItem(props, state, ref); return ( <button {...buttonProps} className="toggle-button" data-pressed={isPressed} data-selected={isSelected} ref={ref} > {props.children} </button> ); } <ToggleButtonGroup> <ToggleButton id="left">Left</ToggleButton> <ToggleButton id="center">Center</ToggleButton> <ToggleButton id="right">Right</ToggleButton> </ToggleButtonGroup> import type {ToggleGroupState} from 'react-stately'; import type { AriaToggleButtonGroupItemProps, AriaToggleButtonGroupProps } from 'react-aria'; import {useToggleGroupState} from 'react-stately'; import { useToggleButtonGroup, useToggleButtonGroupItem } from 'react-aria'; interface ToggleButtonGroupProps extends AriaToggleButtonGroupProps { children: React.ReactNode; } let ToggleButtonGroupContext = React.createContext< | ToggleGroupState | null >(null); function ToggleButtonGroup( props: ToggleButtonGroupProps ) { let state = useToggleGroupState( props ); let ref = React.useRef< HTMLDivElement | null >(null); let { groupProps } = useToggleButtonGroup( props, state, ref ); return ( <div {...groupProps} className="toggle-group" ref={ref} > <ToggleButtonGroupContext.Provider value={state} > {props.children} </ToggleButtonGroupContext.Provider> </div> ); } function ToggleButton( props: AriaToggleButtonGroupItemProps ) { let ref = React.useRef< | HTMLButtonElement | null >(null); let state = React .useContext( ToggleButtonGroupContext )!; let { buttonProps, isPressed, isSelected } = useToggleButtonGroupItem( props, state, ref ); return ( <button {...buttonProps} className="toggle-button" data-pressed={isPressed} data-selected={isSelected} ref={ref} > {props.children} </button> ); } <ToggleButtonGroup> <ToggleButton id="left"> Left </ToggleButton> <ToggleButton id="center"> Center </ToggleButton> <ToggleButton id="right"> Right </ToggleButton> </ToggleButtonGroup> LeftCenterRight Show CSS .toggle-group { display: flex; gap: 4px; &[aria-orientation=vertical] { flex-direction: column; width: fit-content; } } .toggle-button { background: lightgray; color: black; padding: 10px; font-size: 16px; user-select: none; border: none; &[data-pressed=true] { background: gray; } &[data-selected=true] { background: green; color: white; &[data-pressed=true] { background: darkgreen; } } &:disabled { opacity: 0.5; } } .toggle-group { display: flex; gap: 4px; &[aria-orientation=vertical] { flex-direction: column; width: fit-content; } } .toggle-button { background: lightgray; color: black; padding: 10px; font-size: 16px; user-select: none; border: none; &[data-pressed=true] { background: gray; } &[data-selected=true] { background: green; color: white; &[data-pressed=true] { background: darkgreen; } } &:disabled { opacity: 0.5; } } .toggle-group { display: flex; gap: 4px; &[aria-orientation=vertical] { flex-direction: column; width: fit-content; } } .toggle-button { background: lightgray; color: black; padding: 10px; font-size: 16px; user-select: none; border: none; &[data-pressed=true] { background: gray; } &[data-selected=true] { background: green; color: white; &[data-pressed=true] { background: darkgreen; } } &:disabled { opacity: 0.5; } } ## Selection# * * * ToggleButtonGroup supports both single and multiple selection modes. Use `defaultSelectedKeys` to provide a default set of selected items (uncontrolled) and `selectedKeys` to set the selected items (controlled). The value of the selected keys must match the `id` prop of the items. ### Single selection# By default, the `selectionMode` of a `ToggleButtonGroup` is `"single"`. <ToggleButtonGroup defaultSelectedKeys={['list']}> <ToggleButton id="grid">Grid view</ToggleButton> <ToggleButton id="list">List view</ToggleButton> <ToggleButton id="gallery">Gallery view</ToggleButton> </ToggleButtonGroup> <ToggleButtonGroup defaultSelectedKeys={['list']}> <ToggleButton id="grid">Grid view</ToggleButton> <ToggleButton id="list">List view</ToggleButton> <ToggleButton id="gallery">Gallery view</ToggleButton> </ToggleButtonGroup> <ToggleButtonGroup defaultSelectedKeys={[ 'list' ]} > <ToggleButton id="grid"> Grid view </ToggleButton> <ToggleButton id="list"> List view </ToggleButton> <ToggleButton id="gallery"> Gallery view </ToggleButton> </ToggleButtonGroup> Grid viewList viewGallery view ### Multiple selection# Set `selectionMode` prop to `multiple` to allow more than one selection. <ToggleButtonGroup selectionMode="multiple"> <ToggleButton id="bold">Bold</ToggleButton> <ToggleButton id="italic">Italic</ToggleButton> <ToggleButton id="underline">Underline</ToggleButton> </ToggleButtonGroup> <ToggleButtonGroup selectionMode="multiple"> <ToggleButton id="bold">Bold</ToggleButton> <ToggleButton id="italic">Italic</ToggleButton> <ToggleButton id="underline">Underline</ToggleButton> </ToggleButtonGroup> <ToggleButtonGroup selectionMode="multiple"> <ToggleButton id="bold"> Bold </ToggleButton> <ToggleButton id="italic"> Italic </ToggleButton> <ToggleButton id="underline"> Underline </ToggleButton> </ToggleButtonGroup> BoldItalicUnderline ### Controlled selection# The `selectedKeys` prop can be used to make the selected state controlled. import type {Key} from 'react-stately'; function Example() { let [selected, setSelected] = React.useState(new Set<Key>(['bold'])); return ( <> <ToggleButtonGroup selectionMode="multiple" selectedKeys={selected} onSelectionChange={setSelected} > <ToggleButton id="bold">Bold</ToggleButton> <ToggleButton id="italic">Italic</ToggleButton> <ToggleButton id="underline">Underline</ToggleButton> </ToggleButtonGroup> <p>Current selections (controlled): {[...selected].join(', ')}</p> </> ); } import type {Key} from 'react-stately'; function Example() { let [selected, setSelected] = React.useState( new Set<Key>(['bold']) ); return ( <> <ToggleButtonGroup selectionMode="multiple" selectedKeys={selected} onSelectionChange={setSelected} > <ToggleButton id="bold">Bold</ToggleButton> <ToggleButton id="italic">Italic</ToggleButton> <ToggleButton id="underline"> Underline </ToggleButton> </ToggleButtonGroup> <p> Current selections (controlled):{' '} {[...selected].join(', ')} </p> </> ); } import type {Key} from 'react-stately'; function Example() { let [ selected, setSelected ] = React.useState( new Set<Key>([ 'bold' ]) ); return ( <> <ToggleButtonGroup selectionMode="multiple" selectedKeys={selected} onSelectionChange={setSelected} > <ToggleButton id="bold"> Bold </ToggleButton> <ToggleButton id="italic"> Italic </ToggleButton> <ToggleButton id="underline"> Underline </ToggleButton> </ToggleButtonGroup> <p> Current selections (controlled): {' '} {[...selected] .join(', ')} </p> </> ); } BoldItalicUnderline Current selections (controlled): bold ## Disabled# * * * All buttons within a `ToggleButtonGroup` can be disabled using the `isDisabled` prop. <ToggleButtonGroup isDisabled> <ToggleButton id="grid">Grid view</ToggleButton> <ToggleButton id="list">List view</ToggleButton> <ToggleButton id="gallery">Gallery view</ToggleButton> </ToggleButtonGroup> <ToggleButtonGroup isDisabled> <ToggleButton id="grid">Grid view</ToggleButton> <ToggleButton id="list">List view</ToggleButton> <ToggleButton id="gallery">Gallery view</ToggleButton> </ToggleButtonGroup> <ToggleButtonGroup isDisabled > <ToggleButton id="grid"> Grid view </ToggleButton> <ToggleButton id="list"> List view </ToggleButton> <ToggleButton id="gallery"> Gallery view </ToggleButton> </ToggleButtonGroup> Grid viewList viewGallery view Individual items can be disabled using the `isDisabled` prop on each `ToggleButton`. <ToggleButtonGroup> <ToggleButton id="grid">Grid view</ToggleButton> <ToggleButton id="list" isDisabled>List view</ToggleButton> <ToggleButton id="gallery">Gallery view</ToggleButton> </ToggleButtonGroup> <ToggleButtonGroup> <ToggleButton id="grid">Grid view</ToggleButton> <ToggleButton id="list" isDisabled> List view </ToggleButton> <ToggleButton id="gallery">Gallery view</ToggleButton> </ToggleButtonGroup> <ToggleButtonGroup> <ToggleButton id="grid"> Grid view </ToggleButton> <ToggleButton id="list" isDisabled > List view </ToggleButton> <ToggleButton id="gallery"> Gallery view </ToggleButton> </ToggleButtonGroup> Grid viewList viewGallery view ## Orientation# * * * By default, toggle button groups are horizontally oriented. The orientation prop can be set to "vertical" to change the arrow key navigation behavior. <ToggleButtonGroup orientation="vertical"> <ToggleButton id="grid">Grid</ToggleButton> <ToggleButton id="list">List</ToggleButton> <ToggleButton id="gallery">Gallery</ToggleButton> </ToggleButtonGroup> <ToggleButtonGroup orientation="vertical"> <ToggleButton id="grid">Grid</ToggleButton> <ToggleButton id="list">List</ToggleButton> <ToggleButton id="gallery">Gallery</ToggleButton> </ToggleButtonGroup> <ToggleButtonGroup orientation="vertical"> <ToggleButton id="grid"> Grid </ToggleButton> <ToggleButton id="list"> List </ToggleButton> <ToggleButton id="gallery"> Gallery </ToggleButton> </ToggleButtonGroup> GridListGallery ## Accessiblity# * * * A `ToggleButtonGroup` can be labeled using the `aria-label` or `aria-labelledby` props. <ToggleButtonGroup aria-label="Text style"> <ToggleButton id="bold">Bold</ToggleButton> <ToggleButton id="italic">Italic</ToggleButton> <ToggleButton id="underline">Underline</ToggleButton> </ToggleButtonGroup> <ToggleButtonGroup aria-label="Text style"> <ToggleButton id="bold">Bold</ToggleButton> <ToggleButton id="italic">Italic</ToggleButton> <ToggleButton id="underline">Underline</ToggleButton> </ToggleButtonGroup> <ToggleButtonGroup aria-label="Text style"> <ToggleButton id="bold"> Bold </ToggleButton> <ToggleButton id="italic"> Italic </ToggleButton> <ToggleButton id="underline"> Underline </ToggleButton> </ToggleButtonGroup> BoldItalicUnderline --- ## Page: https://react-spectrum.adobe.com/react-aria/useGridList.html # useGridList Provides the behavior and accessibility implementation for a list component with interactive children. A grid list displays data in a single column and enables a user to navigate its contents via directional navigation keys. <table><tbody><tr><th>install</th><td><code>yarn add react-aria</code></td></tr><tr><th>version</th><td>3.41.1</td></tr><tr><th>usage</th><td><code><span>import</span> {useGridList, useGridListItem, useGridListSelectionCheckbox} <span>from</span> <span>'react-aria'</span></code></td></tr></tbody></table> View ARIA pattern W3C View repository GitHub View package NPM ## API# * * * `useGridList<T>( props: AriaGridListOptions <T>, state: ListState <T>, ref: RefObject <HTMLElement | | null> ): GridListAria ``useGridListItem<T>( props: AriaGridListItemOptions , state: ListState <T> | | TreeState <T>, ref: RefObject <FocusableElement | | null> ): GridListItemAria ``useGridListSelectionCheckbox<T>( (props: AriaGridSelectionCheckboxProps , , state: ListState <T> )): GridSelectionCheckboxAria ` ## Features# * * * A list can be built using <ul> or <ol> HTML elements, but does not support any user interactions. HTML lists are meant for static content, rather than lists with rich interactions like focusable elements within rows, keyboard navigation, row selection, etc. `useGridList` helps achieve accessible and interactive list components that can be styled as needed. * **Item selection** – Single or multiple selection, with optional checkboxes, disabled rows, and both `toggle` and `replace` selection behaviors. * **Interactive children** – List items may include interactive elements such as buttons, checkboxes, menus, etc. * **Actions** – Items support optional row actions such as navigation via click, tap, double click, or Enter key. * **Async loading** – Support for loading items asynchronously, with infinite and virtualized scrolling. * **Keyboard navigation** – List items and focusable children can be navigated using the arrow keys, along with page up/down, home/end, etc. Typeahead, auto scrolling, and selection modifier keys are supported as well. * **Touch friendly** – Selection and actions adapt their behavior depending on the device. For example, selection is activated via long press on touch when item actions are present. * **Accessible** – Follows the ARIA grid pattern, with additional selection announcements via an ARIA live region. Extensively tested across many devices and assistive technologies to ensure announcements and behaviors are consistent. **Note**: Use `useGridList` when your list items may contain interactive elements such as buttons, checkboxes, menus, etc. within them. If your list items contain only static content such as text and images, then consider using useListBox instead for a slightly better screen reader experience (especially on mobile). ## Anatomy# * * * A grid list consists of a container element, with rows of data inside. The rows within a list may contain focusable elements or plain text content. If the list supports row selection, each row can optionally include a selection checkbox. The `useGridList` and` useGridListItem `hooks handle keyboard, mouse, and other interactions to support row selection, in list navigation, and overall focus behavior. Those hooks handle exposing the list and its contents to assistive technology using ARIA.` useGridListSelectionCheckbox `handles row selection and associating each checkbox with its respective rows for assistive technology. `useGridList` returns props that you should spread onto the list container element: | Name | Type | Description | | --- | --- | --- | | `gridProps` | `DOMAttributes` | Props for the grid element. | `useGridListItem` returns props for an individual option and its children, along with states you can use for styling: | Name | Type | Description | | --- | --- | --- | | `rowProps` | `DOMAttributes` | Props for the list row element. | | `gridCellProps` | `DOMAttributes` | Props for the grid cell element within the list row. | | `descriptionProps` | `DOMAttributes` | Props for the list item description element, if any. | | `isPressed` | `boolean` | Whether the item is currently in a pressed state. | | `isSelected` | `boolean` | Whether the item is currently selected. | | `isFocused` | `boolean` | Whether the item is currently focused. | | `isDisabled` | `boolean` | Whether the item is non-interactive, i.e. both selection and actions are disabled and the item may not be focused. Dependent on `disabledKeys` and `disabledBehavior`. | | `allowsSelection` | `boolean` | Whether the item may be selected, dependent on `selectionMode`, `disabledKeys`, and `disabledBehavior`. | | `hasAction` | `boolean` | Whether the item has an action, dependent on `onAction`, `disabledKeys`, and `disabledBehavior`. It may also change depending on the current selection state of the list (e.g. when selection is primary). This can be used to enable or disable hover styles or other visual indications of interactivity. | State is managed by the `useListState` hook from `@react-stately/list`. The state object should be passed as an option to each of the above hooks where applicable. Note that an `aria-label` or `aria-labelledby` must be passed to the list to identify the element to assistive technology. ## State management# * * * `useGridList` requires knowledge of the rows in the list in order to handle keyboard navigation and other interactions. It does this using the `Collection` interface, which is a generic interface to access sequential unique keyed data. You can implement this interface yourself, e.g. by using a prop to pass a list of item objects, but` useListState `from `@react-stately/list` implements a JSX based interface for building collections instead. See Collection Components for more information, and Collection Interface for internal details. In addition, `useListState` manages the state necessary for multiple selection and exposes a` SelectionManager `, which makes use of the collection to provide an interface to update the selection state. For more information, see Selection. ## Example# * * * Lists are collection components that include rows as child elements. In this example, we'll use the standard HTML unordered list elements along with hooks from React Aria for each child. You may also use other elements like `<div>` to render these components as appropriate. We'll walk through creating the list container and list item, then add some additional behavior such as selection. The `useGridList` hook will be used to render the outer most list element. It uses the` useListState `hook to construct the list's collection of rows, and manage state such as the focused row and row selection. We'll use the collection to iterate through the rows of the List and render the relevant components, which we'll define below. You may notice the extra `<div>` with `gridCellProps` in our example. This is needed because we are following the ARIA grid pattern, which does not allow rows without any child `gridcell` elements. import {mergeProps, useFocusRing, useGridList, useGridListItem} from 'react-aria'; import {useListState} from 'react-stately'; import {useRef} from 'react'; function List(props) { let state = useListState(props); let ref = useRef<HTMLUListElement | null>(null); let { gridProps } = useGridList(props, state, ref); return ( <ul {...gridProps} ref={ref} className="list"> {[...state.collection].map((item) => ( <ListItem key={item.key} item={item} state={state} /> ))} </ul> ); } function ListItem({ item, state }) { let ref = React.useRef(null); let { rowProps, gridCellProps, isPressed } = useGridListItem( { node: item }, state, ref ); let { isFocusVisible, focusProps } = useFocusRing(); let showCheckbox = state.selectionManager.selectionMode !== 'none' && state.selectionManager.selectionBehavior === 'toggle'; return ( <li {...mergeProps(rowProps, focusProps)} ref={ref} className={`${isPressed ? 'pressed' : ''} ${ isFocusVisible ? 'focus-visible' : '' }`} > <div {...gridCellProps}> {showCheckbox && <ListCheckbox item={item} state={state} />} {item.rendered} </div> </li> ); } import { mergeProps, useFocusRing, useGridList, useGridListItem } from 'react-aria'; import {useListState} from 'react-stately'; import {useRef} from 'react'; function List(props) { let state = useListState(props); let ref = useRef<HTMLUListElement | null>(null); let { gridProps } = useGridList(props, state, ref); return ( <ul {...gridProps} ref={ref} className="list"> {[...state.collection].map((item) => ( <ListItem key={item.key} item={item} state={state} /> ))} </ul> ); } function ListItem({ item, state }) { let ref = React.useRef(null); let { rowProps, gridCellProps, isPressed } = useGridListItem( { node: item }, state, ref ); let { isFocusVisible, focusProps } = useFocusRing(); let showCheckbox = state.selectionManager.selectionMode !== 'none' && state.selectionManager.selectionBehavior === 'toggle'; return ( <li {...mergeProps(rowProps, focusProps)} ref={ref} className={`${isPressed ? 'pressed' : ''} ${ isFocusVisible ? 'focus-visible' : '' }`} > <div {...gridCellProps}> {showCheckbox && ( <ListCheckbox item={item} state={state} /> )} {item.rendered} </div> </li> ); } import { mergeProps, useFocusRing, useGridList, useGridListItem } from 'react-aria'; import {useListState} from 'react-stately'; import {useRef} from 'react'; function List(props) { let state = useListState(props); let ref = useRef< | HTMLUListElement | null >(null); let { gridProps } = useGridList( props, state, ref ); return ( <ul {...gridProps} ref={ref} className="list" > {[ ...state .collection ].map((item) => ( <ListItem key={item.key} item={item} state={state} /> ))} </ul> ); } function ListItem( { item, state } ) { let ref = React.useRef( null ); let { rowProps, gridCellProps, isPressed } = useGridListItem( { node: item }, state, ref ); let { isFocusVisible, focusProps } = useFocusRing(); let showCheckbox = state .selectionManager .selectionMode !== 'none' && state .selectionManager .selectionBehavior === 'toggle'; return ( <li {...mergeProps( rowProps, focusProps )} ref={ref} className={`${ isPressed ? 'pressed' : '' } ${ isFocusVisible ? 'focus-visible' : '' }`} > <div {...gridCellProps} > {showCheckbox && ( <ListCheckbox item={item} state={state} /> )} {item.rendered} </div> </li> ); } Now we can render a basic example list, with multiple selection and interactive children in each item. import {Item} from 'react-stately'; // Reuse the Button from your component library. See below. import {Button} from 'your-component-library'; <List aria-label="Example List" selectionMode="multiple" selectionBehavior="replace" > <Item textValue="Charizard"> Charizard <Button onPress={() => alert(`Info for Charizard...`)}>Info</Button> </Item> <Item textValue="Blastoise"> Blastoise <Button onPress={() => alert(`Info for Blastoise...`)}>Info</Button> </Item> <Item textValue="Venusaur"> Venusaur <Button onPress={() => alert(`Info for Venusaur...`)}>Info</Button> </Item> <Item textValue="Pikachu"> Pikachu <Button onPress={() => alert(`Info for Pikachu...`)}>Info</Button> </Item> </List> import {Item} from 'react-stately'; // Reuse the Button from your component library. See below. import {Button} from 'your-component-library'; <List aria-label="Example List" selectionMode="multiple" selectionBehavior="replace" > <Item textValue="Charizard"> Charizard <Button onPress={() => alert(`Info for Charizard...`)} > Info </Button> </Item> <Item textValue="Blastoise"> Blastoise <Button onPress={() => alert(`Info for Blastoise...`)} > Info </Button> </Item> <Item textValue="Venusaur"> Venusaur <Button onPress={() => alert(`Info for Venusaur...`)}> Info </Button> </Item> <Item textValue="Pikachu"> Pikachu <Button onPress={() => alert(`Info for Pikachu...`)}> Info </Button> </Item> </List> import {Item} from 'react-stately'; // Reuse the Button from your component library. See below. import {Button} from 'your-component-library'; <List aria-label="Example List" selectionMode="multiple" selectionBehavior="replace" > <Item textValue="Charizard"> Charizard <Button onPress={() => alert( `Info for Charizard...` )} > Info </Button> </Item> <Item textValue="Blastoise"> Blastoise <Button onPress={() => alert( `Info for Blastoise...` )} > Info </Button> </Item> <Item textValue="Venusaur"> Venusaur <Button onPress={() => alert( `Info for Venusaur...` )} > Info </Button> </Item> <Item textValue="Pikachu"> Pikachu <Button onPress={() => alert( `Info for Pikachu...` )} > Info </Button> </Item> </List> * CharizardInfo * BlastoiseInfo * VenusaurInfo * PikachuInfo Show CSS .list { padding: 0; list-style: none; background: var(--page-background); border: 1px solid var(--spectrum-global-color-gray-400); max-width: 400px; min-width: 200px; max-height: 250px; overflow: auto; } .list li { padding: 8px; outline: none; cursor: default; } .list li:nth-child(2n) { background: var(--spectrum-alias-highlight-hover); } .list li.pressed { background: var(--spectrum-global-color-gray-200); } .list li[aria-selected=true] { background: slateblue; color: white; } .list li.focus-visible { outline: 2px solid slateblue; outline-offset: -3px; } .list li.focus-visible[aria-selected=true] { outline-color: white; } .list li[aria-disabled] { opacity: 0.4; } .list [role=gridcell] { display: flex; align-items: center; gap: 4px; } .list li button { margin-left: auto; } /* iOS Safari has a bug that prevents accent-color: white from working. */ @supports not (-webkit-touch-callout: none) { .list li input[type=checkbox] { accent-color: white; } } .list { padding: 0; list-style: none; background: var(--page-background); border: 1px solid var(--spectrum-global-color-gray-400); max-width: 400px; min-width: 200px; max-height: 250px; overflow: auto; } .list li { padding: 8px; outline: none; cursor: default; } .list li:nth-child(2n) { background: var(--spectrum-alias-highlight-hover); } .list li.pressed { background: var(--spectrum-global-color-gray-200); } .list li[aria-selected=true] { background: slateblue; color: white; } .list li.focus-visible { outline: 2px solid slateblue; outline-offset: -3px; } .list li.focus-visible[aria-selected=true] { outline-color: white; } .list li[aria-disabled] { opacity: 0.4; } .list [role=gridcell] { display: flex; align-items: center; gap: 4px; } .list li button { margin-left: auto; } /* iOS Safari has a bug that prevents accent-color: white from working. */ @supports not (-webkit-touch-callout: none) { .list li input[type=checkbox] { accent-color: white; } } .list { padding: 0; list-style: none; background: var(--page-background); border: 1px solid var(--spectrum-global-color-gray-400); max-width: 400px; min-width: 200px; max-height: 250px; overflow: auto; } .list li { padding: 8px; outline: none; cursor: default; } .list li:nth-child(2n) { background: var(--spectrum-alias-highlight-hover); } .list li.pressed { background: var(--spectrum-global-color-gray-200); } .list li[aria-selected=true] { background: slateblue; color: white; } .list li.focus-visible { outline: 2px solid slateblue; outline-offset: -3px; } .list li.focus-visible[aria-selected=true] { outline-color: white; } .list li[aria-disabled] { opacity: 0.4; } .list [role=gridcell] { display: flex; align-items: center; gap: 4px; } .list li button { margin-left: auto; } /* iOS Safari has a bug that prevents accent-color: white from working. */ @supports not (-webkit-touch-callout: none) { .list li input[type=checkbox] { accent-color: white; } } ### Adding selection checkboxes# Next, let's add support for selection checkboxes to allow the user to select items explicitly. This is done using the `useGridListSelectionCheckbox` hook. It is passed the `key` of the item it is contained within. When the user checks or unchecks the checkbox, the row will be added or removed from the List's selection. The `Checkbox` component used in this example is independent and can be used separately from `useGridList`. The code is available below. import {useGridListSelectionCheckbox} from 'react-aria'; // Reuse the Checkbox from your component library. See below for details. import {Checkbox} from 'your-component-library'; function ListCheckbox({ item, state }) { let { checkboxProps } = useGridListSelectionCheckbox( { key: item.key }, state ); return <Checkbox {...checkboxProps} />; } import {useGridListSelectionCheckbox} from 'react-aria'; // Reuse the Checkbox from your component library. See below for details. import {Checkbox} from 'your-component-library'; function ListCheckbox({ item, state }) { let { checkboxProps } = useGridListSelectionCheckbox({ key: item.key }, state); return <Checkbox {...checkboxProps} />; } import {useGridListSelectionCheckbox} from 'react-aria'; // Reuse the Checkbox from your component library. See below for details. import {Checkbox} from 'your-component-library'; function ListCheckbox( { item, state } ) { let { checkboxProps } = useGridListSelectionCheckbox( { key: item.key }, state ); return ( <Checkbox {...checkboxProps} /> ); } The following example shows an example list with multiple selection using checkboxes and the default `toggle` selection behavior. <List aria-label="List with selection" selectionMode="multiple"> <Item textValue="Charizard"> Charizard <Button onPress={() => alert(`Info for Charizard...`)}>Info</Button> </Item> <Item textValue="Blastoise"> Blastoise <Button onPress={() => alert(`Info for Blastoise...`)}>Info</Button> </Item> <Item textValue="Venusaur"> Venusaur <Button onPress={() => alert(`Info for Venusaur...`)}>Info</Button> </Item> <Item textValue="Pikachu"> Pikachu <Button onPress={() => alert(`Info for Pikachu...`)}>Info</Button> </Item> </List> <List aria-label="List with selection" selectionMode="multiple" > <Item textValue="Charizard"> Charizard <Button onPress={() => alert(`Info for Charizard...`)} > Info </Button> </Item> <Item textValue="Blastoise"> Blastoise <Button onPress={() => alert(`Info for Blastoise...`)} > Info </Button> </Item> <Item textValue="Venusaur"> Venusaur <Button onPress={() => alert(`Info for Venusaur...`)}> Info </Button> </Item> <Item textValue="Pikachu"> Pikachu <Button onPress={() => alert(`Info for Pikachu...`)}> Info </Button> </Item> </List> <List aria-label="List with selection" selectionMode="multiple" > <Item textValue="Charizard"> Charizard <Button onPress={() => alert( `Info for Charizard...` )} > Info </Button> </Item> <Item textValue="Blastoise"> Blastoise <Button onPress={() => alert( `Info for Blastoise...` )} > Info </Button> </Item> <Item textValue="Venusaur"> Venusaur <Button onPress={() => alert( `Info for Venusaur...` )} > Info </Button> </Item> <Item textValue="Pikachu"> Pikachu <Button onPress={() => alert( `Info for Pikachu...` )} > Info </Button> </Item> </List> * CharizardInfo * BlastoiseInfo * VenusaurInfo * PikachuInfo And that's it! We now have a fully interactive List component that can support keyboard navigation, single or multiple selection. In addition, it is fully accessible for screen readers and other assistive technology. See below for more examples of how to use the List component that we've built. ### Checkbox# The `Checkbox` component is used in the above example for row selection. It is built using the useCheckbox hook, and can be shared with many other components. Show code import {useToggleState} from 'react-stately'; import {useCheckbox} from 'react-aria'; function Checkbox(props) { let inputRef = useRef(null); let { inputProps } = useCheckbox( props, useToggleState(props), inputRef ); return <input {...inputProps} ref={inputRef} />; } import {useToggleState} from 'react-stately'; import {useCheckbox} from 'react-aria'; function Checkbox(props) { let inputRef = useRef(null); let { inputProps } = useCheckbox( props, useToggleState(props), inputRef ); return <input {...inputProps} ref={inputRef} />; } import {useToggleState} from 'react-stately'; import {useCheckbox} from 'react-aria'; function Checkbox( props ) { let inputRef = useRef( null ); let { inputProps } = useCheckbox( props, useToggleState( props ), inputRef ); return ( <input {...inputProps} ref={inputRef} /> ); } ### Button# The `Button` component is used in the above example to show how rows can contain interactive elements. It is built using the useButton hook, and can be shared with many other components. Show code import {useButton} from 'react-aria'; function Button(props) { let ref = React.useRef(null); let { buttonProps } = useButton(props, ref); return <button {...buttonProps} ref={ref}>{props.children}</button>; } import {useButton} from 'react-aria'; function Button(props) { let ref = React.useRef(null); let { buttonProps } = useButton(props, ref); return ( <button {...buttonProps} ref={ref}> {props.children} </button> ); } import {useButton} from 'react-aria'; function Button(props) { let ref = React.useRef( null ); let { buttonProps } = useButton( props, ref ); return ( <button {...buttonProps} ref={ref} > {props.children} </button> ); } ## Usage# * * * ### Dynamic collections# So far, our examples have shown static collections, where the data is hard coded. Dynamic collections, as shown below, can be used when the data comes from an external data source such as an API, or updates over time. In the example below, the rows are provided to the List via a render function. function ExampleList(props) { let rows = [ { id: 1, name: 'Games' }, { id: 2, name: 'Program Files' }, { id: 3, name: 'bootmgr' }, { id: 4, name: 'log.txt' } ]; return ( <List aria-label="Example dynamic collection List" selectionMode="multiple" items={rows} {...props} > {(item) => ( <Item textValue={item.name}> {item.name} <Button onPress={() => alert(`Info for ${item.name}...`)}> Info </Button> </Item> )} </List> ); } function ExampleList(props) { let rows = [ { id: 1, name: 'Games' }, { id: 2, name: 'Program Files' }, { id: 3, name: 'bootmgr' }, { id: 4, name: 'log.txt' } ]; return ( <List aria-label="Example dynamic collection List" selectionMode="multiple" items={rows} {...props} > {(item) => ( <Item textValue={item.name}> {item.name} <Button onPress={() => alert(`Info for ${item.name}...`)} > Info </Button> </Item> )} </List> ); } function ExampleList( props ) { let rows = [ { id: 1, name: 'Games' }, { id: 2, name: 'Program Files' }, { id: 3, name: 'bootmgr' }, { id: 4, name: 'log.txt' } ]; return ( <List aria-label="Example dynamic collection List" selectionMode="multiple" items={rows} {...props} > {(item) => ( <Item textValue={item .name} > {item.name} <Button onPress={() => alert( `Info for ${item.name}...` )} > Info </Button> </Item> )} </List> ); } * GamesInfo * Program FilesInfo * bootmgrInfo * log.txtInfo ### Single selection# By default, `useListState` doesn't allow row selection but this can be enabled using the `selectionMode` prop. Use `defaultSelectedKeys` to provide a default set of selected rows. Note that the value of the selected keys must match the `key` prop of the row. The example below enables single selection mode, and uses `defaultSelectedKeys` to select the row with key equal to "2". A user can click on a different row to change the selection, or click on the same row again to deselect it entirely. // Using the example above <ExampleList aria-label="List with single selection" selectionMode="single" selectionBehavior="replace" defaultSelectedKeys={[2]} /> // Using the example above <ExampleList aria-label="List with single selection" selectionMode="single" selectionBehavior="replace" defaultSelectedKeys={[2]} /> // Using the example above <ExampleList aria-label="List with single selection" selectionMode="single" selectionBehavior="replace" defaultSelectedKeys={[ 2 ]} /> * GamesInfo * Program FilesInfo * bootmgrInfo * log.txtInfo ### Multiple selection# Multiple selection can be enabled by setting `selectionMode` to `multiple`. <ExampleList aria-label="List with multiple selection" selectionMode="multiple" defaultSelectedKeys={[2, 4]} /> <ExampleList aria-label="List with multiple selection" selectionMode="multiple" defaultSelectedKeys={[2, 4]} /> <ExampleList aria-label="List with multiple selection" selectionMode="multiple" defaultSelectedKeys={[ 2, 4 ]} /> * GamesInfo * Program FilesInfo * bootmgrInfo * log.txtInfo ### Disallow empty selection# `useGridList` also supports a `disallowEmptySelection` prop which forces the user to have at least one row in the List selected at all times. In this mode, if a single row is selected and the user presses it, it will not be deselected. <ExampleList aria-label="List with disallowed empty selection" selectionMode="multiple" defaultSelectedKeys={[2]} disallowEmptySelection /> <ExampleList aria-label="List with disallowed empty selection" selectionMode="multiple" defaultSelectedKeys={[2]} disallowEmptySelection /> <ExampleList aria-label="List with disallowed empty selection" selectionMode="multiple" defaultSelectedKeys={[ 2 ]} disallowEmptySelection /> * GamesInfo * Program FilesInfo * bootmgrInfo * log.txtInfo ### Controlled selection# To programmatically control row selection, use the `selectedKeys` prop paired with the `onSelectionChange` callback. The `key` prop from the selected rows will be passed into the callback when the row is pressed, allowing you to update state accordingly. function PokemonList(props) { let rows = [ { id: 1, name: 'Charizard' }, { id: 2, name: 'Blastoise' }, { id: 3, name: 'Venusaur' }, { id: 4, name: 'Pikachu' } ]; let [selectedKeys, setSelectedKeys] = React.useState(new Set([2])); return ( <List aria-label="List with controlled selection" items={rows} selectionMode="multiple" selectedKeys={selectedKeys} onSelectionChange={setSelectedKeys} {...props} > {(item) => <Item>{item.name}</Item>} </List> ); } function PokemonList(props) { let rows = [ { id: 1, name: 'Charizard' }, { id: 2, name: 'Blastoise' }, { id: 3, name: 'Venusaur' }, { id: 4, name: 'Pikachu' } ]; let [selectedKeys, setSelectedKeys] = React.useState( new Set([2]) ); return ( <List aria-label="List with controlled selection" items={rows} selectionMode="multiple" selectedKeys={selectedKeys} onSelectionChange={setSelectedKeys} {...props} > {(item) => <Item>{item.name}</Item>} </List> ); } function PokemonList( props ) { let rows = [ { id: 1, name: 'Charizard' }, { id: 2, name: 'Blastoise' }, { id: 3, name: 'Venusaur' }, { id: 4, name: 'Pikachu' } ]; let [ selectedKeys, setSelectedKeys ] = React.useState( new Set([2]) ); return ( <List aria-label="List with controlled selection" items={rows} selectionMode="multiple" selectedKeys={selectedKeys} onSelectionChange={setSelectedKeys} {...props} > {(item) => ( <Item> {item.name} </Item> )} </List> ); } * Charizard * Blastoise * Venusaur * Pikachu ### Disabled rows# You can disable specific rows by providing an array of keys to `useListState` via the `disabledKeys` prop. This will disable all interactions on disabled rows, unless the `disabledBehavior` prop is used to change this behavior. Note that you are responsible for the styling of disabled rows, however, the selection checkbox will be automatically disabled. // Using the example above <PokemonList aria-label="List with disabled rows" selectionMode="multiple" disabledKeys={[3]} /> // Using the example above <PokemonList aria-label="List with disabled rows" selectionMode="multiple" disabledKeys={[3]} /> // Using the example above <PokemonList aria-label="List with disabled rows" selectionMode="multiple" disabledKeys={[3]} /> * Charizard * Blastoise * Venusaur * Pikachu When `disabledBehavior` is set to `selection`, interactions such as focus, dragging, or actions can still be performed on disabled rows. <PokemonList aria-label="List with selection disabled for disabled rows" selectionMode="multiple" disabledKeys={[3]} disabledBehavior="selection" /> <PokemonList aria-label="List with selection disabled for disabled rows" selectionMode="multiple" disabledKeys={[3]} disabledBehavior="selection" /> <PokemonList aria-label="List with selection disabled for disabled rows" selectionMode="multiple" disabledKeys={[3]} disabledBehavior="selection" /> * Charizard * Blastoise * Venusaur * Pikachu ### Selection behavior# By default, `useGridList` uses the `"toggle"` selection behavior, which behaves like a checkbox group: clicking, tapping, or pressing the Space or Enter keys toggles selection for the focused row. Using the arrow keys moves focus but does not change selection. The `"toggle"` selection mode is often paired with a checkbox in each row as an explicit affordance for selection. When `selectionBehavior` is set to `"replace"`, clicking a row with the mouse replaces the selection with only that row. Using the arrow keys moves both focus and selection. To select multiple rows, modifier keys such as Ctrl, Cmd, and Shift can be used. On touch screen devices, selection always behaves as toggle since modifier keys may not be available. These selection styles implement the behaviors defined in Aria Practices. <PokemonList aria-label="List with replace selection behavior" selectionMode="multiple" selectionBehavior="replace" /> <PokemonList aria-label="List with replace selection behavior" selectionMode="multiple" selectionBehavior="replace" /> <PokemonList aria-label="List with replace selection behavior" selectionMode="multiple" selectionBehavior="replace" /> * Charizard * Blastoise * Venusaur * Pikachu ### Row actions# `useGridList` supports row actions via the `onAction` prop, which is useful for functionality such as navigation. When nothing is selected, the list performs actions by default when clicking or tapping a row. Items may be selected using the checkbox, or by long pressing on touch devices. When at least one item is selected, the list is in selection mode, and clicking or tapping a row toggles the selection. Actions may also be triggered via the Enter key, and selection using the Space key. This behavior is slightly different when `selectionBehavior="replace"`, where single clicking selects the row and actions are performed via double click. Touch and keyboard behaviors are unaffected. <div style={{ display: 'flex', flexWrap: 'wrap', gap: 24 }}> <ExampleList aria-label="Checkbox selection list with row actions" selectionMode="multiple" selectionBehavior="toggle" onAction={(key) => alert(`Opening item ${key}...`)} /> <ExampleList aria-label="Highlight selection list with row actions" selectionMode="multiple" selectionBehavior="replace" onAction={(key) => alert(`Opening item ${key}...`)} /> </div> <div style={{ display: 'flex', flexWrap: 'wrap', gap: 24 }}> <ExampleList aria-label="Checkbox selection list with row actions" selectionMode="multiple" selectionBehavior="toggle" onAction={(key) => alert(`Opening item ${key}...`)} /> <ExampleList aria-label="Highlight selection list with row actions" selectionMode="multiple" selectionBehavior="replace" onAction={(key) => alert(`Opening item ${key}...`)} /> </div> <div style={{ display: 'flex', flexWrap: 'wrap', gap: 24 }} > <ExampleList aria-label="Checkbox selection list with row actions" selectionMode="multiple" selectionBehavior="toggle" onAction={(key) => alert( `Opening item ${key}...` )} /> <ExampleList aria-label="Highlight selection list with row actions" selectionMode="multiple" selectionBehavior="replace" onAction={(key) => alert( `Opening item ${key}...` )} /> </div> * GamesInfo * Program FilesInfo * bootmgrInfo * log.txtInfo * GamesInfo * Program FilesInfo * bootmgrInfo * log.txtInfo ### Links# Items in a GridList may also be links to another page or website. This can be achieved by passing the `href` prop to the `<Item>` component. Links behave the same way as described above for row actions depending on the `selectionMode` and `selectionBehavior`. <List aria-label="Links" selectionMode="multiple"> <Item href="https://adobe.com/" target="_blank">Adobe</Item> <Item href="https://apple.com/" target="_blank">Apple</Item> <Item href="https://google.com/" target="_blank">Google</Item> <Item href="https://microsoft.com/" target="_blank">Microsoft</Item> </List> <List aria-label="Links" selectionMode="multiple"> <Item href="https://adobe.com/" target="_blank"> Adobe </Item> <Item href="https://apple.com/" target="_blank"> Apple </Item> <Item href="https://google.com/" target="_blank"> Google </Item> <Item href="https://microsoft.com/" target="_blank"> Microsoft </Item> </List> <List aria-label="Links" selectionMode="multiple" > <Item href="https://adobe.com/" target="_blank" > Adobe </Item> <Item href="https://apple.com/" target="_blank" > Apple </Item> <Item href="https://google.com/" target="_blank" > Google </Item> <Item href="https://microsoft.com/" target="_blank" > Microsoft </Item> </List> * Adobe * Apple * Google * Microsoft #### Client side routing# The `<Item>` component works with frameworks and client side routers like Next.js and React Router. As with other React Aria components that support links, this works via the `RouterProvider` component at the root of your app. See the client side routing guide to learn how to set this up. ### Asynchronous loading# This example uses the useAsyncList hook to handle asynchronous loading of data from a server. You may additionally want to display a spinner to indicate the loading state to the user, or support features like infinite scroll to load more data. import {useAsyncList} from 'react-stately'; function AsyncList() { let list = useAsyncList({ async load({ signal, cursor }) { if (cursor) { cursor = cursor.replace(/^http:\/\//i, 'https://'); } let res = await fetch( cursor || `https://swapi.py4e.com/api/people/?search=`, { signal } ); let json = await res.json(); return { items: json.results, cursor: json.next }; } }); return ( <List selectionMode="multiple" aria-label="Async loading ListView example" items={list.items} > {(item) => <Item key={item.name}>{item.name}</Item>} </List> ); } import {useAsyncList} from 'react-stately'; function AsyncList() { let list = useAsyncList({ async load({ signal, cursor }) { if (cursor) { cursor = cursor.replace(/^http:\/\//i, 'https://'); } let res = await fetch( cursor || `https://swapi.py4e.com/api/people/?search=`, { signal } ); let json = await res.json(); return { items: json.results, cursor: json.next }; } }); return ( <List selectionMode="multiple" aria-label="Async loading ListView example" items={list.items} > {(item) => <Item key={item.name}>{item.name}</Item>} </List> ); } import {useAsyncList} from 'react-stately'; function AsyncList() { let list = useAsyncList({ async load( { signal, cursor } ) { if (cursor) { cursor = cursor .replace( /^http:\/\//i, 'https://' ); } let res = await fetch( cursor || `https://swapi.py4e.com/api/people/?search=`, { signal } ); let json = await res .json(); return { items: json.results, cursor: json.next }; } }); return ( <List selectionMode="multiple" aria-label="Async loading ListView example" items={list.items} > {(item) => ( <Item key={item.name} > {item.name} </Item> )} </List> ); } * Luke Skywalker * C-3PO * R2-D2 * Darth Vader * Leia Organa * Owen Lars * Beru Whitesun lars * R5-D4 * Biggs Darklighter * Obi-Wan Kenobi ## Internationalization# * * * `useGridList` handles some aspects of internationalization automatically. For example, type to select is implemented with an Intl.Collator for internationalized string matching, and keyboard navigation is mirrored in right-to-left languages. You are responsible for localizing all text content within the List. ### RTL# In right-to-left languages, the list layout should be mirrored. The row contents should be ordered from right to left and the row's text alignment should be inverted. Ensure that your CSS accounts for this. --- ## Page: https://react-spectrum.adobe.com/react-aria/useListBox.html # useListBox Provides the behavior and accessibility implementation for a listbox component. A listbox displays a list of options and allows a user to select one or more of them. <table><tbody><tr><th>install</th><td><code>yarn add react-aria</code></td></tr><tr><th>version</th><td>3.41.1</td></tr><tr><th>usage</th><td><code><span>import</span> {useListBox, useOption, useListBoxSection} <span>from</span> <span>'react-aria'</span></code></td></tr></tbody></table> View ARIA pattern W3C View repository GitHub View package NPM ## API# * * * `useListBox<T>( props: AriaListBoxOptions <T>, state: ListState <T>, ref: RefObject <HTMLElement | | null> ): ListBoxAria ``useOption<T>( props: AriaOptionProps , state: ListState <T>, ref: RefObject <FocusableElement | | null> ): OptionAria ``useListBoxSection( (props: AriaListBoxSectionProps )): ListBoxSectionAria ` ## Features# * * * A listbox can be built using the <select> and <option> HTML elements, but this is not possible to style consistently cross browser. `useListBox` helps achieve accessible listbox components that can be styled as needed. Note: `useListBox` only handles the list itself. For a dropdown similar to a `<select>`, see useSelect. * Exposed to assistive technology as a `listbox` using ARIA * Support for single, multiple, or no selection * Support for disabled items * Support for sections * Labeling support for accessibility * Support for mouse, touch, and keyboard interactions * Tab stop focus management * Keyboard navigation support including arrow keys, home/end, page up/down, select all, and clear * Automatic scrolling support during keyboard navigation * Typeahead to allow focusing options by typing text * Support for use with virtualized lists ## Anatomy# * * * A listbox consists of a container element, with a list of options or groups inside. `useListBox`, `useOption`, and `useListBoxSection` handle exposing this to assistive technology using ARIA, along with handling keyboard, mouse, and interactions to support selection and focus behavior. `useListBox` returns props that you should spread onto the list container element, along with props for an optional visual label: | Name | Type | Description | | --- | --- | --- | | `listBoxProps` | `DOMAttributes` | Props for the listbox element. | | `labelProps` | `DOMAttributes` | Props for the listbox's visual label element (if any). | `useOption` returns props for an individual option and its children, along with states you can use for styling: | Name | Type | Description | | --- | --- | --- | | `optionProps` | `DOMAttributes` | Props for the option element. | | `labelProps` | `DOMAttributes` | Props for the main text element inside the option. | | `descriptionProps` | `DOMAttributes` | Props for the description text element inside the option, if any. | | `isFocused` | `boolean` | Whether the option is currently focused. | | `isFocusVisible` | `boolean` | Whether the option is keyboard focused. | | `isPressed` | `boolean` | Whether the item is currently in a pressed state. | | `isSelected` | `boolean` | Whether the item is currently selected. | | `isDisabled` | `boolean` | Whether the item is non-interactive, i.e. both selection and actions are disabled and the item may not be focused. Dependent on `disabledKeys` and `disabledBehavior`. | | `allowsSelection` | `boolean` | Whether the item may be selected, dependent on `selectionMode`, `disabledKeys`, and `disabledBehavior`. | | `hasAction` | `boolean` | Whether the item has an action, dependent on `onAction`, `disabledKeys`, and `disabledBehavior`. It may also change depending on the current selection state of the list (e.g. when selection is primary). This can be used to enable or disable hover styles or other visual indications of interactivity. | `useListBoxSection` returns props for a section: | Name | Type | Description | | --- | --- | --- | | `itemProps` | `DOMAttributes` | Props for the wrapper list item. | | `headingProps` | `DOMAttributes` | Props for the heading element, if any. | | `groupProps` | `DOMAttributes` | Props for the group element. | State is managed by the `useListState` hook from `@react-stately/list`. The state object should be passed as an option to each of the above hooks. If a listbox, options, or group does not have a visible label, an `aria-label` or `aria-labelledby` prop must be passed instead to identify the element to assistive technology. ## State management# * * * `useListBox` requires knowledge of the options in the listbox in order to handle keyboard navigation and other interactions. It does this using the `Collection` interface, which is a generic interface to access sequential unique keyed data. You can implement this interface yourself, e.g. by using a prop to pass a list of item objects, but` useListState `from `@react-stately/list` implements a JSX based interface for building collections instead. See Collection Components for more information, and Collection Interface for internal details. In addition, `useListState` manages the state necessary for multiple selection and exposes a` SelectionManager `, which makes use of the collection to provide an interface to update the selection state. For more information, see Selection. ## Example# * * * This example uses HTML `<ul>` and `<li>` elements to represent the list, and applies props from `useListBox` and` useOption `. For each item in the collection in state, either an `Option` or `ListBoxSection` (defined below) is rendered according to the item's `type` property. import type {AriaListBoxProps} from 'react-aria'; import {Item, useListState} from 'react-stately'; import {mergeProps, useFocusRing, useListBox, useOption} from 'react-aria'; function ListBox<T extends object>(props: AriaListBoxProps<T>) { // Create state based on the incoming props let state = useListState(props); // Get props for the listbox element let ref = React.useRef(null); let { listBoxProps, labelProps } = useListBox(props, state, ref); return ( <> <div {...labelProps}>{props.label}</div> <ul {...listBoxProps} ref={ref}> {[...state.collection].map((item) => ( item.type === 'section' ? <ListBoxSection key={item.key} section={item} state={state} /> : <Option key={item.key} item={item} state={state} /> ))} </ul> </> ); } function Option({ item, state }) { // Get props for the option element let ref = React.useRef(null); let { optionProps } = useOption({ key: item.key }, state, ref); // Determine whether we should show a keyboard // focus ring for accessibility let { isFocusVisible, focusProps } = useFocusRing(); return ( <li {...mergeProps(optionProps, focusProps)} ref={ref} data-focus-visible={isFocusVisible} > {item.rendered} </li> ); } <ListBox label="Alignment" selectionMode="single"> <Item>Left</Item> <Item>Middle</Item> <Item>Right</Item> </ListBox> import type {AriaListBoxProps} from 'react-aria'; import {Item, useListState} from 'react-stately'; import { mergeProps, useFocusRing, useListBox, useOption } from 'react-aria'; function ListBox<T extends object>( props: AriaListBoxProps<T> ) { // Create state based on the incoming props let state = useListState(props); // Get props for the listbox element let ref = React.useRef(null); let { listBoxProps, labelProps } = useListBox( props, state, ref ); return ( <> <div {...labelProps}>{props.label}</div> <ul {...listBoxProps} ref={ref}> {[...state.collection].map((item) => ( item.type === 'section' ? ( <ListBoxSection key={item.key} section={item} state={state} /> ) : ( <Option key={item.key} item={item} state={state} /> ) ))} </ul> </> ); } function Option({ item, state }) { // Get props for the option element let ref = React.useRef(null); let { optionProps } = useOption( { key: item.key }, state, ref ); // Determine whether we should show a keyboard // focus ring for accessibility let { isFocusVisible, focusProps } = useFocusRing(); return ( <li {...mergeProps(optionProps, focusProps)} ref={ref} data-focus-visible={isFocusVisible} > {item.rendered} </li> ); } <ListBox label="Alignment" selectionMode="single"> <Item>Left</Item> <Item>Middle</Item> <Item>Right</Item> </ListBox> import type {AriaListBoxProps} from 'react-aria'; import { Item, useListState } from 'react-stately'; import { mergeProps, useFocusRing, useListBox, useOption } from 'react-aria'; function ListBox< T extends object >( props: AriaListBoxProps<T> ) { // Create state based on the incoming props let state = useListState(props); // Get props for the listbox element let ref = React.useRef( null ); let { listBoxProps, labelProps } = useListBox( props, state, ref ); return ( <> <div {...labelProps} > {props.label} </div> <ul {...listBoxProps} ref={ref} > {[ ...state .collection ].map((item) => ( item.type === 'section' ? ( <ListBoxSection key={item .key} section={item} state={state} /> ) : ( <Option key={item .key} item={item} state={state} /> ) ))} </ul> </> ); } function Option( { item, state } ) { // Get props for the option element let ref = React.useRef( null ); let { optionProps } = useOption( { key: item.key }, state, ref ); // Determine whether we should show a keyboard // focus ring for accessibility let { isFocusVisible, focusProps } = useFocusRing(); return ( <li {...mergeProps( optionProps, focusProps )} ref={ref} data-focus-visible={isFocusVisible} > {item.rendered} </li> ); } <ListBox label="Alignment" selectionMode="single" > <Item>Left</Item> <Item>Middle</Item> <Item>Right</Item> </ListBox> Alignment * Left * Middle * Right Show CSS [role=listbox] { padding: 0; margin: 5px 0; list-style: none; border: 1px solid gray; max-width: 250px; max-height: 300px; overflow: auto; } [role=option] { display: block; padding: 2px 5px; outline: none; cursor: default; color: inherit; &[data-focus-visible=true] { outline: 2px solid orange; } &[aria-selected=true] { background: blueviolet; color: white; } &[aria-disabled] { color: #aaa; } } [role=listbox] { padding: 0; margin: 5px 0; list-style: none; border: 1px solid gray; max-width: 250px; max-height: 300px; overflow: auto; } [role=option] { display: block; padding: 2px 5px; outline: none; cursor: default; color: inherit; &[data-focus-visible=true] { outline: 2px solid orange; } &[aria-selected=true] { background: blueviolet; color: white; } &[aria-disabled] { color: #aaa; } } [role=listbox] { padding: 0; margin: 5px 0; list-style: none; border: 1px solid gray; max-width: 250px; max-height: 300px; overflow: auto; } [role=option] { display: block; padding: 2px 5px; outline: none; cursor: default; color: inherit; &[data-focus-visible=true] { outline: 2px solid orange; } &[aria-selected=true] { background: blueviolet; color: white; } &[aria-disabled] { color: #aaa; } } ## Dynamic collections# * * * `ListBox` follows the Collection Components API, accepting both static and dynamic collections. The example above shows static collections, which can be used when the full list of options is known ahead of time. Dynamic collections, as shown below, can be used when the options come from an external data source such as an API call, or update over time. As seen below, an iterable list of options is passed to the ListBox using the `items` prop. Each item accepts a `key` prop, which is passed to the `onSelectionChange` handler to identify the selected item. Alternatively, if the item objects contain an `id` property, as shown in the example below, then this is used automatically and a `key` prop is not required. function Example() { let options = [ { id: 1, name: 'Aardvark' }, { id: 2, name: 'Cat' }, { id: 3, name: 'Dog' }, { id: 4, name: 'Kangaroo' }, { id: 5, name: 'Koala' }, { id: 6, name: 'Penguin' }, { id: 7, name: 'Snake' }, { id: 8, name: 'Turtle' }, { id: 9, name: 'Wombat' } ]; return ( <ListBox label="Animals" items={options} selectionMode="single"> {(item) => <Item>{item.name}</Item>} </ListBox> ); } function Example() { let options = [ { id: 1, name: 'Aardvark' }, { id: 2, name: 'Cat' }, { id: 3, name: 'Dog' }, { id: 4, name: 'Kangaroo' }, { id: 5, name: 'Koala' }, { id: 6, name: 'Penguin' }, { id: 7, name: 'Snake' }, { id: 8, name: 'Turtle' }, { id: 9, name: 'Wombat' } ]; return ( <ListBox label="Animals" items={options} selectionMode="single" > {(item) => <Item>{item.name}</Item>} </ListBox> ); } function Example() { let options = [ { id: 1, name: 'Aardvark' }, { id: 2, name: 'Cat' }, { id: 3, name: 'Dog' }, { id: 4, name: 'Kangaroo' }, { id: 5, name: 'Koala' }, { id: 6, name: 'Penguin' }, { id: 7, name: 'Snake' }, { id: 8, name: 'Turtle' }, { id: 9, name: 'Wombat' } ]; return ( <ListBox label="Animals" items={options} selectionMode="single" > {(item) => ( <Item> {item.name} </Item> )} </ListBox> ); } Animals * Aardvark * Cat * Dog * Kangaroo * Koala * Penguin * Snake * Turtle * Wombat ## Selection# * * * ListBox supports multiple selection modes. By default, selection is disabled, however this can be changed using the `selectionMode` prop. Use `defaultSelectedKeys` to provide a default set of selected items (uncontrolled) and `selectedKeys` to set the selected items (controlled). The value of the selected keys must match the `key` prop of the items. See the `react-stately` Selection docs for more details. import type {Selection} from 'react-stately'; function Example() { let [selected, setSelected] = React.useState<Selection>(new Set(['cheese'])); return ( <> <ListBox label="Choose sandwich contents" selectionMode="multiple" selectedKeys={selected} onSelectionChange={setSelected} > <Item key="lettuce">Lettuce</Item> <Item key="tomato">Tomato</Item> <Item key="cheese">Cheese</Item> <Item key="tuna">Tuna Salad</Item> <Item key="egg">Egg Salad</Item> <Item key="ham">Ham</Item> </ListBox> <p> Current selection (controlled):{' '} {selected === 'all' ? 'all' : [...selected].join(', ')} </p> </> ); } import type {Selection} from 'react-stately'; function Example() { let [selected, setSelected] = React.useState<Selection>( new Set(['cheese']) ); return ( <> <ListBox label="Choose sandwich contents" selectionMode="multiple" selectedKeys={selected} onSelectionChange={setSelected} > <Item key="lettuce">Lettuce</Item> <Item key="tomato">Tomato</Item> <Item key="cheese">Cheese</Item> <Item key="tuna">Tuna Salad</Item> <Item key="egg">Egg Salad</Item> <Item key="ham">Ham</Item> </ListBox> <p> Current selection (controlled): {selected === 'all' ? 'all' : [...selected].join(', ')} </p> </> ); } import type {Selection} from 'react-stately'; function Example() { let [ selected, setSelected ] = React.useState< Selection >(new Set(['cheese'])); return ( <> <ListBox label="Choose sandwich contents" selectionMode="multiple" selectedKeys={selected} onSelectionChange={setSelected} > <Item key="lettuce"> Lettuce </Item> <Item key="tomato"> Tomato </Item> <Item key="cheese"> Cheese </Item> <Item key="tuna"> Tuna Salad </Item> <Item key="egg"> Egg Salad </Item> <Item key="ham"> Ham </Item> </ListBox> <p> Current selection (controlled): {' '} {selected === 'all' ? 'all' : [...selected] .join(', ')} </p> </> ); } Choose sandwich contents * Lettuce * Tomato * Cheese * Tuna Salad * Egg Salad * Ham Current selection (controlled): cheese ### Selection behavior# By default, `useListBox` uses the `"toggle"` selection behavior, which behaves like a checkbox group: clicking, tapping, or pressing the Space or Enter keys toggles selection for the focused row. Using the arrow keys moves focus but does not change selection. When `selectionBehavior` is set to `"replace"`, clicking a row with the mouse replaces the selection with only that row. Using the arrow keys moves both focus and selection. To select multiple rows, modifier keys such as Ctrl, Cmd, and Shift can be used. On touch screen devices, selection always behaves as toggle since modifier keys may not be available. These selection behaviors are defined in Aria Practices. <ListBox label="Choose sandwich contents" selectionMode="multiple" selectionBehavior="replace" > <Item key="lettuce">Lettuce</Item> <Item key="tomato">Tomato</Item> <Item key="cheese">Cheese</Item> <Item key="tuna">Tuna Salad</Item> <Item key="egg">Egg Salad</Item> <Item key="ham">Ham</Item> </ListBox> <ListBox label="Choose sandwich contents" selectionMode="multiple" selectionBehavior="replace" > <Item key="lettuce">Lettuce</Item> <Item key="tomato">Tomato</Item> <Item key="cheese">Cheese</Item> <Item key="tuna">Tuna Salad</Item> <Item key="egg">Egg Salad</Item> <Item key="ham">Ham</Item> </ListBox> <ListBox label="Choose sandwich contents" selectionMode="multiple" selectionBehavior="replace" > <Item key="lettuce"> Lettuce </Item> <Item key="tomato"> Tomato </Item> <Item key="cheese"> Cheese </Item> <Item key="tuna"> Tuna Salad </Item> <Item key="egg"> Egg Salad </Item> <Item key="ham"> Ham </Item> </ListBox> Choose sandwich contents * Lettuce * Tomato * Cheese * Tuna Salad * Egg Salad * Ham ## Sections# * * * ListBox supports sections with separators and headings in order to group options. Sections can be used by wrapping groups of Items in a `Section` component. Each `Section` takes a `title` and `key` prop. To implement sections, implement the `ListBoxSection` component referenced above using the `useListBoxSection` hook. It will include four extra elements: an `<li>` between the sections to represent the separator, an `<li>` to contain the heading `<span>` element, and a `<ul>` to contain the child items. This structure is necessary to ensure HTML semantics are correct. import {useListBoxSection} from 'react-aria'; function ListBoxSection({ section, state }) { let { itemProps, headingProps, groupProps } = useListBoxSection({ heading: section.rendered, 'aria-label': section['aria-label'] }); // If the section is not the first, add a separator element to provide visual separation. // The heading is rendered inside an <li> element, which contains // a <ul> with the child items. return ( <> {section.key !== state.collection.getFirstKey() && ( <li role="presentation" style={{ borderTop: '1px solid gray', margin: '2px 5px' }} /> )} <li {...itemProps}> {section.rendered && ( <span {...headingProps} style={{ fontWeight: 'bold', fontSize: '1.1em', padding: '2px 5px' }} > {section.rendered} </span> )} <ul {...groupProps} style={{ padding: 0, listStyle: 'none' }} > {[...section.childNodes].map((node) => ( <Option key={node.key} item={node} state={state} /> ))} </ul> </li> </> ); } import {useListBoxSection} from 'react-aria'; function ListBoxSection({ section, state }) { let { itemProps, headingProps, groupProps } = useListBoxSection({ heading: section.rendered, 'aria-label': section['aria-label'] }); // If the section is not the first, add a separator element to provide visual separation. // The heading is rendered inside an <li> element, which contains // a <ul> with the child items. return ( <> {section.key !== state.collection.getFirstKey() && ( <li role="presentation" style={{ borderTop: '1px solid gray', margin: '2px 5px' }} /> )} <li {...itemProps}> {section.rendered && ( <span {...headingProps} style={{ fontWeight: 'bold', fontSize: '1.1em', padding: '2px 5px' }} > {section.rendered} </span> )} <ul {...groupProps} style={{ padding: 0, listStyle: 'none' }} > {[...section.childNodes].map((node) => ( <Option key={node.key} item={node} state={state} /> ))} </ul> </li> </> ); } import {useListBoxSection} from 'react-aria'; function ListBoxSection( { section, state } ) { let { itemProps, headingProps, groupProps } = useListBoxSection({ heading: section.rendered, 'aria-label': section[ 'aria-label' ] }); // If the section is not the first, add a separator element to provide visual separation. // The heading is rendered inside an <li> element, which contains // a <ul> with the child items. return ( <> {section.key !== state .collection .getFirstKey() && ( <li role="presentation" style={{ borderTop: '1px solid gray', margin: '2px 5px' }} /> )} <li {...itemProps}> {section .rendered && ( <span {...headingProps} style={{ fontWeight: 'bold', fontSize: '1.1em', padding: '2px 5px' }} > {section .rendered} </span> )} <ul {...groupProps} style={{ padding: 0, listStyle: 'none' }} > {[ ...section .childNodes ].map( (node) => ( <Option key={node .key} item={node} state={state} /> ) )} </ul> </li> </> ); } ### Static items# With this in place, we can now render a static ListBox with multiple sections: import {Section} from 'react-stately'; <ListBox label="Choose sandwich contents" selectionMode="multiple"> <Section title="Veggies"> <Item key="lettuce">Lettuce</Item> <Item key="tomato">Tomato</Item> <Item key="onion">Onion</Item> </Section> <Section title="Protein"> <Item key="ham">Ham</Item> <Item key="tuna">Tuna</Item> <Item key="tofu">Tofu</Item> </Section> <Section title="Condiments"> <Item key="mayo">Mayonaise</Item> <Item key="mustard">Mustard</Item> <Item key="ranch">Ranch</Item> </Section> </ListBox> import {Section} from 'react-stately'; <ListBox label="Choose sandwich contents" selectionMode="multiple" > <Section title="Veggies"> <Item key="lettuce">Lettuce</Item> <Item key="tomato">Tomato</Item> <Item key="onion">Onion</Item> </Section> <Section title="Protein"> <Item key="ham">Ham</Item> <Item key="tuna">Tuna</Item> <Item key="tofu">Tofu</Item> </Section> <Section title="Condiments"> <Item key="mayo">Mayonaise</Item> <Item key="mustard">Mustard</Item> <Item key="ranch">Ranch</Item> </Section> </ListBox> import {Section} from 'react-stately'; <ListBox label="Choose sandwich contents" selectionMode="multiple" > <Section title="Veggies"> <Item key="lettuce"> Lettuce </Item> <Item key="tomato"> Tomato </Item> <Item key="onion"> Onion </Item> </Section> <Section title="Protein"> <Item key="ham"> Ham </Item> <Item key="tuna"> Tuna </Item> <Item key="tofu"> Tofu </Item> </Section> <Section title="Condiments"> <Item key="mayo"> Mayonaise </Item> <Item key="mustard"> Mustard </Item> <Item key="ranch"> Ranch </Item> </Section> </ListBox> Choose sandwich contents * Veggies * Lettuce * Tomato * Onion * Protein * Ham * Tuna * Tofu * Condiments * Mayonaise * Mustard * Ranch ### Dynamic items# The above example shows sections with static items. Sections can also be populated from a hierarchical data structure. Similarly to the props on ListBox, `<Section>` takes an array of data using the `items` prop. import type {Selection} from 'react-stately'; function Example() { let options = [ {name: 'Australian', children: [ {id: 2, name: 'Koala'}, {id: 3, name: 'Kangaroo'}, {id: 4, name: 'Platypus'} ]}, {name: 'American', children: [ {id: 6, name: 'Bald Eagle'}, {id: 7, name: 'Bison'}, {id: 8, name: 'Skunk'} ]} ]; let [selected, setSelected] = React.useState<Selection>(new Set()); return ( <ListBox label="Pick an animal" items={options} selectedKeys={selected} selectionMode="single" onSelectionChange={setSelected}> {item => ( <Section key={item.name} items={item.children} title={item.name}> {item => <Item>{item.name}</Item>} </Section> )} </ListBox> ); } import type {Selection} from 'react-stately'; function Example() { let options = [ { name: 'Australian', children: [ { id: 2, name: 'Koala' }, { id: 3, name: 'Kangaroo' }, { id: 4, name: 'Platypus' } ] }, { name: 'American', children: [ { id: 6, name: 'Bald Eagle' }, { id: 7, name: 'Bison' }, { id: 8, name: 'Skunk' } ] } ]; let [selected, setSelected] = React.useState<Selection>( new Set() ); return ( <ListBox label="Pick an animal" items={options} selectedKeys={selected} selectionMode="single" onSelectionChange={setSelected} > {(item) => ( <Section key={item.name} items={item.children} title={item.name} > {(item) => <Item>{item.name}</Item>} </Section> )} </ListBox> ); } import type {Selection} from 'react-stately'; function Example() { let options = [ { name: 'Australian', children: [ { id: 2, name: 'Koala' }, { id: 3, name: 'Kangaroo' }, { id: 4, name: 'Platypus' } ] }, { name: 'American', children: [ { id: 6, name: 'Bald Eagle' }, { id: 7, name: 'Bison' }, { id: 8, name: 'Skunk' } ] } ]; let [ selected, setSelected ] = React.useState< Selection >(new Set()); return ( <ListBox label="Pick an animal" items={options} selectedKeys={selected} selectionMode="single" onSelectionChange={setSelected} > {(item) => ( <Section key={item.name} items={item .children} title={item .name} > {(item) => ( <Item> {item.name} </Item> )} </Section> )} </ListBox> ); } Pick an animal * Australian * Koala * Kangaroo * Platypus * American * Bald Eagle * Bison * Skunk ### Accessibility# Sections without a `title` must provide an `aria-label` for accessibility. ## Complex options# * * * By default, options that only contain text will be labeled by the contents of the option. For options that have more complex content (e.g. icons, multiple lines of text, etc.), use `labelProps` and `descriptionProps` from `useOption` as needed to apply to the main text element of the option and its description. This improves screen reader announcement. **NOTE: listbox options cannot contain interactive content (e.g. buttons, checkboxes, etc.). For these cases, see useGridList instead.** To implement this, we'll update the `Option` component to apply the ARIA properties returned by `useOption` to the appropriate elements. In this example, we'll pull them out of `props.children` and use `React.cloneElement` to apply the props, but you may want to use a more robust approach (e.g. context). function Option({ item, state }) { let ref = React.useRef(null); let { optionProps, labelProps, descriptionProps } = useOption( { key: item.key }, state, ref ); let { isFocusVisible, focusProps } = useFocusRing(); // Pull out the two expected children. We will clone them // and add the necessary props for accessibility. let [title, description] = item.rendered; return ( <li {...mergeProps(optionProps, focusProps)} ref={ref} data-focus-visible={isFocusVisible} > {React.cloneElement(title, labelProps)} {React.cloneElement(description, descriptionProps)} </li> ); } <ListBox label="Text alignment" selectionMode="single"> <Item textValue="Align Left"> <div> <strong>Align Left</strong> </div> <div>Align the selected text to the left</div> </Item> <Item textValue="Align Center"> <div> <strong>Align Center</strong> </div> <div>Align the selected text center</div> </Item> <Item textValue="Align Right"> <div> <strong>Align Right</strong> </div> <div>Align the selected text to the right</div> </Item> </ListBox> function Option({ item, state }) { let ref = React.useRef(null); let { optionProps, labelProps, descriptionProps } = useOption({ key: item.key }, state, ref); let { isFocusVisible, focusProps } = useFocusRing(); // Pull out the two expected children. We will clone them // and add the necessary props for accessibility. let [title, description] = item.rendered; return ( <li {...mergeProps(optionProps, focusProps)} ref={ref} data-focus-visible={isFocusVisible} > {React.cloneElement(title, labelProps)} {React.cloneElement(description, descriptionProps)} </li> ); } <ListBox label="Text alignment" selectionMode="single"> <Item textValue="Align Left"> <div> <strong>Align Left</strong> </div> <div>Align the selected text to the left</div> </Item> <Item textValue="Align Center"> <div> <strong>Align Center</strong> </div> <div>Align the selected text center</div> </Item> <Item textValue="Align Right"> <div> <strong>Align Right</strong> </div> <div>Align the selected text to the right</div> </Item> </ListBox> function Option( { item, state } ) { let ref = React.useRef( null ); let { optionProps, labelProps, descriptionProps } = useOption( { key: item.key }, state, ref ); let { isFocusVisible, focusProps } = useFocusRing(); // Pull out the two expected children. We will clone them // and add the necessary props for accessibility. let [ title, description ] = item.rendered; return ( <li {...mergeProps( optionProps, focusProps )} ref={ref} data-focus-visible={isFocusVisible} > {React .cloneElement( title, labelProps )} {React .cloneElement( description, descriptionProps )} </li> ); } <ListBox label="Text alignment" selectionMode="single" > <Item textValue="Align Left"> <div> <strong> Align Left </strong> </div> <div> Align the selected text to the left </div> </Item> <Item textValue="Align Center"> <div> <strong> Align Center </strong> </div> <div> Align the selected text center </div> </Item> <Item textValue="Align Right"> <div> <strong> Align Right </strong> </div> <div> Align the selected text to the right </div> </Item> </ListBox> Text alignment * **Align Left** Align the selected text to the left * **Align Center** Align the selected text center * **Align Right** Align the selected text to the right ## Asynchronous loading# * * * This example uses the useAsyncList hook to handle asynchronous loading of data from a server. You may additionally want to display a spinner to indicate the loading state to the user, or support features like infinite scroll to load more data. import {useAsyncList} from 'react-stately'; interface Pokemon { name: string; } function AsyncLoadingExample() { let list = useAsyncList<Pokemon>({ async load({ signal }) { let res = await fetch( `https://pokeapi.co/api/v2/pokemon`, { signal } ); let json = await res.json(); return { items: json.results }; } }); return ( <ListBox label="Pick a Pokemon" items={list.items} selectionMode="single"> {(item) => <Item key={item.name}>{item.name}</Item>} </ListBox> ); } import {useAsyncList} from 'react-stately'; interface Pokemon { name: string; } function AsyncLoadingExample() { let list = useAsyncList<Pokemon>({ async load({ signal }) { let res = await fetch( `https://pokeapi.co/api/v2/pokemon`, { signal } ); let json = await res.json(); return { items: json.results }; } }); return ( <ListBox label="Pick a Pokemon" items={list.items} selectionMode="single" > {(item) => <Item key={item.name}>{item.name}</Item>} </ListBox> ); } import {useAsyncList} from 'react-stately'; interface Pokemon { name: string; } function AsyncLoadingExample() { let list = useAsyncList< Pokemon >({ async load( { signal } ) { let res = await fetch( `https://pokeapi.co/api/v2/pokemon`, { signal } ); let json = await res .json(); return { items: json.results }; } }); return ( <ListBox label="Pick a Pokemon" items={list.items} selectionMode="single" > {(item) => ( <Item key={item.name} > {item.name} </Item> )} </ListBox> ); } Pick a Pokemon * bulbasaur * ivysaur * venusaur * charmander * charmeleon * charizard * squirtle * wartortle * blastoise * caterpie * metapod * butterfree * weedle * kakuna * beedrill * pidgey * pidgeotto * pidgeot * rattata * raticate ## Links# * * * By default, interacting with an item in a ListBox triggers `onSelectionChange`. Alternatively, items may be links to another page or website. This can be achieved by passing the `href` prop to the `<Item>` component. This example shows how to update the `Option` component with support for rendering an `<a>` element if an `href` prop is passed to the item. Note that you'll also need to render the `ListBox` as a `<div>` instead of a `<ul>`, since an `<a>` inside a `<ul>` is not valid HTML. function Option({item, state}) { let ref = React.useRef(null); let {optionProps} = useOption({key: item.key}, state, ref); let {isFocusVisible, focusProps} = useFocusRing(); let ElementType: React.ElementType = item.props.href ? 'a' : 'div'; return ( <ElementType {...mergeProps(optionProps, focusProps)} ref={ref} data-focus-visible={isFocusVisible}> {item.rendered} </ElementType> ); } <ListBox aria-label="Links"> <Item href="https://adobe.com/" target="_blank">Adobe</Item> <Item href="https://apple.com/" target="_blank">Apple</Item> <Item href="https://google.com/" target="_blank">Google</Item> <Item href="https://microsoft.com/" target="_blank">Microsoft</Item> </ListBox> function Option({ item, state }) { let ref = React.useRef(null); let { optionProps } = useOption( { key: item.key }, state, ref ); let { isFocusVisible, focusProps } = useFocusRing(); let ElementType: React.ElementType = item.props.href ? 'a' : 'div'; return ( <ElementType {...mergeProps(optionProps, focusProps)} ref={ref} data-focus-visible={isFocusVisible} > {item.rendered} </ElementType> ); } <ListBox aria-label="Links"> <Item href="https://adobe.com/" target="_blank"> Adobe </Item> <Item href="https://apple.com/" target="_blank"> Apple </Item> <Item href="https://google.com/" target="_blank"> Google </Item> <Item href="https://microsoft.com/" target="_blank"> Microsoft </Item> </ListBox> function Option( { item, state } ) { let ref = React.useRef( null ); let { optionProps } = useOption( { key: item.key }, state, ref ); let { isFocusVisible, focusProps } = useFocusRing(); let ElementType: React.ElementType = item.props.href ? 'a' : 'div'; return ( <ElementType {...mergeProps( optionProps, focusProps )} ref={ref} data-focus-visible={isFocusVisible} > {item.rendered} </ElementType> ); } <ListBox aria-label="Links"> <Item href="https://adobe.com/" target="_blank" > Adobe </Item> <Item href="https://apple.com/" target="_blank" > Apple </Item> <Item href="https://google.com/" target="_blank" > Google </Item> <Item href="https://microsoft.com/" target="_blank" > Microsoft </Item> </ListBox> AdobeAppleGoogleMicrosoft By default, link items in a ListBox are not selectable, and only perform navigation when the user interacts with them. However, with the "replace" selection behavior, items will be selected when single clicking or pressing the Space key, and navigate to the link when double clicking or pressing the Enter key. <ListBox aria-label="Links" selectionMode="multiple" selectionBehavior="replace" > <Item href="https://adobe.com/" target="_blank">Adobe</Item> <Item href="https://apple.com/" target="_blank">Apple</Item> <Item href="https://google.com/" target="_blank">Google</Item> <Item href="https://microsoft.com/" target="_blank">Microsoft</Item> </ListBox> <ListBox aria-label="Links" selectionMode="multiple" selectionBehavior="replace" > <Item href="https://adobe.com/" target="_blank"> Adobe </Item> <Item href="https://apple.com/" target="_blank"> Apple </Item> <Item href="https://google.com/" target="_blank"> Google </Item> <Item href="https://microsoft.com/" target="_blank"> Microsoft </Item> </ListBox> <ListBox aria-label="Links" selectionMode="multiple" selectionBehavior="replace" > <Item href="https://adobe.com/" target="_blank" > Adobe </Item> <Item href="https://apple.com/" target="_blank" > Apple </Item> <Item href="https://google.com/" target="_blank" > Google </Item> <Item href="https://microsoft.com/" target="_blank" > Microsoft </Item> </ListBox> AdobeAppleGoogleMicrosoft ### Client side routing# The `<Item>` component works with frameworks and client side routers like Next.js and React Router. As with other React Aria components that support links, this works via the `RouterProvider` component at the root of your app. See the client side routing guide to learn how to set this up. ## Disabled items# * * * `useListBox` supports marking items as disabled using the `disabledKeys` prop. Each key in this list corresponds with the `key` prop passed to the `Item` component, or automatically derived from the values passed to the `items` prop. See Collections for more details. Disabled items are not focusable, selectable, or keyboard navigable. The `isDisabled` property returned by `useOption` can be used to style the item appropriately. <ListBox label="Choose sandwich contents" selectionMode="multiple" disabledKeys={['tuna']} > <Item key="lettuce">Lettuce</Item> <Item key="tomato">Tomato</Item> <Item key="cheese">Cheese</Item> <Item key="tuna">Tuna Salad</Item> <Item key="egg">Egg Salad</Item> <Item key="ham">Ham</Item> </ListBox> <ListBox label="Choose sandwich contents" selectionMode="multiple" disabledKeys={['tuna']} > <Item key="lettuce">Lettuce</Item> <Item key="tomato">Tomato</Item> <Item key="cheese">Cheese</Item> <Item key="tuna">Tuna Salad</Item> <Item key="egg">Egg Salad</Item> <Item key="ham">Ham</Item> </ListBox> <ListBox label="Choose sandwich contents" selectionMode="multiple" disabledKeys={[ 'tuna' ]} > <Item key="lettuce"> Lettuce </Item> <Item key="tomato"> Tomato </Item> <Item key="cheese"> Cheese </Item> <Item key="tuna"> Tuna Salad </Item> <Item key="egg"> Egg Salad </Item> <Item key="ham"> Ham </Item> </ListBox> Choose sandwich contents * Lettuce * Tomato * Cheese * Tuna Salad * Egg Salad * Ham ## Internationalization# * * * `useListBox` handles some aspects of internationalization automatically. For example, type to select is implemented with an Intl.Collator for internationalized string matching. You are responsible for localizing all labels and option content that is passed into the listbox. ### RTL# In right-to-left languages, the listbox options should be mirrored. The text content should be aligned to the right. Ensure that your CSS accounts for this. --- ## Page: https://react-spectrum.adobe.com/react-aria/useMenu.html # useMenu Provides the behavior and accessibility implementation for a menu component. A menu displays a list of actions or options that a user can choose. <table><tbody><tr><th>install</th><td><code>yarn add react-aria</code></td></tr><tr><th>version</th><td>3.41.1</td></tr><tr><th>usage</th><td><code><span>import</span> {useMenuTrigger, useMenu, useMenuItem, useMenuSection} <span>from</span> <span>'react-aria'</span></code></td></tr></tbody></table> View ARIA pattern W3C View repository GitHub View package NPM ## API# * * * `useMenuTrigger<T>( props: AriaMenuTriggerProps , state: MenuTriggerState , ref: RefObject <Element | | null> ): MenuTriggerAria <T>` `useMenu<T>( props: AriaMenuOptions <T>, state: TreeState <T>, ref: RefObject <HTMLElement | | null> ): MenuAria ``useMenuItem<T>( props: AriaMenuItemProps , state: TreeState <T>, ref: RefObject <FocusableElement | | null> ): MenuItemAria ``useMenuSection( (props: AriaMenuSectionProps )): MenuSectionAria ` ## Features# * * * There is no native element to implement a menu in HTML that is widely supported. `useMenuTrigger` and `useMenu` help achieve accessible menu components that can be styled as needed. * Exposed to assistive technology as a button with a `menu` using ARIA * Support for single, multiple, or no selection * Support for disabled items * Support for sections * Complex item labeling support for accessibility * Keyboard navigation support including arrow keys, home/end, page up/down * Automatic scrolling support during keyboard navigation * Keyboard support for opening the menu using the arrow keys, including automatically focusing the first or last item accordingly * Typeahead to allow focusing items by typing text * Support for use with virtualized lists ## Anatomy# * * * A menu trigger consists of a button or other trigger element combined with a popup menu, with a list of menu items or groups inside. `useMenuTrigger`, `useMenu`, `useMenuItem`, and `useMenuSection` handle exposing this to assistive technology using ARIA, along with handling keyboard, mouse, and interactions to support selection and focus behavior. `useMenuTrigger` returns props that you should spread onto the appropriate element: | Name | Type | Description | | --- | --- | --- | | `menuTriggerProps` | ` AriaButtonProps ` | Props for the menu trigger element. | | `menuProps` | ` AriaMenuOptions <T>` | Props for the menu. | `useMenu` returns props that you should spread onto the menu container element: | Name | Type | Description | | --- | --- | --- | | `menuProps` | `DOMAttributes` | Props for the menu element. | `useMenuItem` returns props for an individual item and its children: | Name | Type | Description | | --- | --- | --- | | `menuItemProps` | `DOMAttributes` | Props for the menu item element. | | `labelProps` | `DOMAttributes` | Props for the main text element inside the menu item. | | `descriptionProps` | `DOMAttributes` | Props for the description text element inside the menu item, if any. | | `keyboardShortcutProps` | `DOMAttributes` | Props for the keyboard shortcut text element inside the item, if any. | | `isFocused` | `boolean` | Whether the item is currently focused. | | `isFocusVisible` | `boolean` | Whether the item is keyboard focused. | | `isSelected` | `boolean` | Whether the item is currently selected. | | `isPressed` | `boolean` | Whether the item is currently in a pressed state. | | `isDisabled` | `boolean` | Whether the item is disabled. | `useMenuSection` returns props for a section: | Name | Type | Description | | --- | --- | --- | | `itemProps` | `DOMAttributes` | Props for the wrapper list item. | | `headingProps` | `DOMAttributes` | Props for the heading element, if any. | | `groupProps` | `DOMAttributes` | Props for the group element. | State for the trigger is managed by the `useMenuTriggerState` hook from `@react-stately/menu`. State for the menu itself is managed by the` useTreeState `hook from `@react-stately/tree`. These state objects should be passed to the appropriate React Aria hooks. If a menu, menu item, or group does not have a visible label, an `aria-label` or `aria-labelledby` prop must be passed instead to identify the element to assistive technology. ## Example# * * * A menu consists of several components: a menu button to toggle the menu popup, and the menu itself, which contains items or sections of items. We'll go through each component one by one. ### MenuButton# We'll start with the `MenuButton` component, which is what will trigger our menu to appear. This uses the `useMenuTrigger` and` useMenuTriggerState `hooks. The `Popover` and `Button` components used in this example are independent, and can be shared by many other components. The code is available below, and documentation is available on the corresponding pages. import type {MenuTriggerProps} from 'react-stately'; import {useMenuTrigger} from 'react-aria'; import {Item, useMenuTriggerState} from 'react-stately'; // Reuse the Popover, and Button from your component library. See below for details. import {Button, Popover} from 'your-component-library'; interface MenuButtonProps<T> extends AriaMenuProps<T>, MenuTriggerProps { label?: string; } function MenuButton<T extends object>(props: MenuButtonProps<T>) { // Create state based on the incoming props let state = useMenuTriggerState(props); // Get props for the button and menu elements let ref = React.useRef(null); let { menuTriggerProps, menuProps } = useMenuTrigger<T>({}, state, ref); return ( <> <Button {...menuTriggerProps} buttonRef={ref} style={{ height: 30, fontSize: 14 }} > {props.label} <span aria-hidden="true" style={{ paddingLeft: 5 }}>▼</span> </Button> {state.isOpen && ( <Popover state={state} triggerRef={ref} placement="bottom start"> <Menu {...props} {...menuProps} /> </Popover> )} </> ); } import type {MenuTriggerProps} from 'react-stately'; import {useMenuTrigger} from 'react-aria'; import {Item, useMenuTriggerState} from 'react-stately'; // Reuse the Popover, and Button from your component library. See below for details. import {Button, Popover} from 'your-component-library'; interface MenuButtonProps<T> extends AriaMenuProps<T>, MenuTriggerProps { label?: string; } function MenuButton<T extends object>( props: MenuButtonProps<T> ) { // Create state based on the incoming props let state = useMenuTriggerState(props); // Get props for the button and menu elements let ref = React.useRef(null); let { menuTriggerProps, menuProps } = useMenuTrigger<T>( {}, state, ref ); return ( <> <Button {...menuTriggerProps} buttonRef={ref} style={{ height: 30, fontSize: 14 }} > {props.label} <span aria-hidden="true" style={{ paddingLeft: 5 }}> ▼ </span> </Button> {state.isOpen && ( <Popover state={state} triggerRef={ref} placement="bottom start" > <Menu {...props} {...menuProps} /> </Popover> )} </> ); } import type {MenuTriggerProps} from 'react-stately'; import {useMenuTrigger} from 'react-aria'; import { Item, useMenuTriggerState } from 'react-stately'; // Reuse the Popover, and Button from your component library. See below for details. import { Button, Popover } from 'your-component-library'; interface MenuButtonProps< T > extends AriaMenuProps<T>, MenuTriggerProps { label?: string; } function MenuButton< T extends object >( props: MenuButtonProps< T > ) { // Create state based on the incoming props let state = useMenuTriggerState( props ); // Get props for the button and menu elements let ref = React.useRef( null ); let { menuTriggerProps, menuProps } = useMenuTrigger<T>( {}, state, ref ); return ( <> <Button {...menuTriggerProps} buttonRef={ref} style={{ height: 30, fontSize: 14 }} > {props.label} <span aria-hidden="true" style={{ paddingLeft: 5 }} > ▼ </span> </Button> {state.isOpen && ( <Popover state={state} triggerRef={ref} placement="bottom start" > <Menu {...props} {...menuProps} /> </Popover> )} </> ); } ### Menu# Next, let's implement the `Menu` component. This will appear inside the `Popover` when the user presses the button. It is built using the `useMenu` and` useTreeState `hooks. For each item in the collection in state, we render either a `MenuItem` or `MenuSection` (defined below) according to the item's `type` property. import type {AriaMenuProps} from 'react-aria'; import {useTreeState} from 'react-stately'; import {useMenu} from 'react-aria'; function Menu<T extends object>(props: AriaMenuProps<T>) { // Create menu state based on the incoming props let state = useTreeState(props); // Get props for the menu element let ref = React.useRef(null); let { menuProps } = useMenu(props, state, ref); return ( <ul {...menuProps} ref={ref}> {[...state.collection].map((item) => ( item.type === 'section' ? <MenuSection key={item.key} section={item} state={state} /> : <MenuItem key={item.key} item={item} state={state} /> ))} </ul> ); } import type {AriaMenuProps} from 'react-aria'; import {useTreeState} from 'react-stately'; import {useMenu} from 'react-aria'; function Menu<T extends object>(props: AriaMenuProps<T>) { // Create menu state based on the incoming props let state = useTreeState(props); // Get props for the menu element let ref = React.useRef(null); let { menuProps } = useMenu(props, state, ref); return ( <ul {...menuProps} ref={ref}> {[...state.collection].map((item) => ( item.type === 'section' ? ( <MenuSection key={item.key} section={item} state={state} /> ) : ( <MenuItem key={item.key} item={item} state={state} /> ) ))} </ul> ); } import type {AriaMenuProps} from 'react-aria'; import {useTreeState} from 'react-stately'; import {useMenu} from 'react-aria'; function Menu< T extends object >( props: AriaMenuProps<T> ) { // Create menu state based on the incoming props let state = useTreeState(props); // Get props for the menu element let ref = React.useRef( null ); let { menuProps } = useMenu( props, state, ref ); return ( <ul {...menuProps} ref={ref} > {[ ...state .collection ].map((item) => ( item.type === 'section' ? ( <MenuSection key={item .key} section={item} state={state} /> ) : ( <MenuItem key={item .key} item={item} state={state} /> ) ))} </ul> ); } ### MenuItem# Now let's implement `MenuItem`. This is built using `useMenuItem` , and the `state` object passed via props from `Menu`. import {useMenuItem} from 'react-aria'; function MenuItem({ item, state }) { // Get props for the menu item element let ref = React.useRef(null); let { menuItemProps, isSelected } = useMenuItem( { key: item.key }, state, ref ); return ( <li {...menuItemProps} ref={ref}> {item.rendered} {isSelected && <span aria-hidden="true">✅</span>} </li> ); } import {useMenuItem} from 'react-aria'; function MenuItem({ item, state }) { // Get props for the menu item element let ref = React.useRef(null); let { menuItemProps, isSelected } = useMenuItem( { key: item.key }, state, ref ); return ( <li {...menuItemProps} ref={ref}> {item.rendered} {isSelected && <span aria-hidden="true">✅</span>} </li> ); } import {useMenuItem} from 'react-aria'; function MenuItem( { item, state } ) { // Get props for the menu item element let ref = React.useRef( null ); let { menuItemProps, isSelected } = useMenuItem( { key: item.key }, state, ref ); return ( <li {...menuItemProps} ref={ref} > {item.rendered} {isSelected && ( <span aria-hidden="true"> ✅ </span> )} </li> ); } Now we can render a simple menu with actionable items: <MenuButton label="Actions" onAction={alert}> <Item key="copy">Copy</Item> <Item key="cut">Cut</Item> <Item key="paste">Paste</Item> </MenuButton> <MenuButton label="Actions" onAction={alert}> <Item key="copy">Copy</Item> <Item key="cut">Cut</Item> <Item key="paste">Paste</Item> </MenuButton> <MenuButton label="Actions" onAction={alert} > <Item key="copy"> Copy </Item> <Item key="cut"> Cut </Item> <Item key="paste"> Paste </Item> </MenuButton> Actions▼ Show CSS [role=menu] { margin: 0; padding: 0; list-style: none; width: 200px; } [role=menuitem], [role=menuitemradio], [role=menuitemcheckbox] { padding: 2px 5px; outline: none; cursor: default; display: flex; justify-content: space-between; color: black; &:focus { background: gray; color: white; } &[aria-disabled] { color: gray; } } [role=menu] { margin: 0; padding: 0; list-style: none; width: 200px; } [role=menuitem], [role=menuitemradio], [role=menuitemcheckbox] { padding: 2px 5px; outline: none; cursor: default; display: flex; justify-content: space-between; color: black; &:focus { background: gray; color: white; } &[aria-disabled] { color: gray; } } [role=menu] { margin: 0; padding: 0; list-style: none; width: 200px; } [role=menuitem], [role=menuitemradio], [role=menuitemcheckbox] { padding: 2px 5px; outline: none; cursor: default; display: flex; justify-content: space-between; color: black; &:focus { background: gray; color: white; } &[aria-disabled] { color: gray; } } ### Popover# The `Popover` component is used to contain the menu. It can be shared between many other components, including ComboBox, Select, and others. See usePopover for more examples of popovers. Show code import {DismissButton, Overlay, usePopover} from 'react-aria'; import type {AriaPopoverProps} from 'react-aria'; import type {OverlayTriggerState} from 'react-stately'; interface PopoverProps extends Omit<AriaPopoverProps, 'popoverRef'> { children: React.ReactNode; state: OverlayTriggerState; } function Popover({ children, state, ...props }: PopoverProps) { let popoverRef = React.useRef(null); let { popoverProps, underlayProps } = usePopover({ ...props, popoverRef }, state); return ( <Overlay> <div {...underlayProps} style={{ position: 'fixed', inset: 0 }} /> <div {...popoverProps} ref={popoverRef} style={{ ...popoverProps.style, background: 'lightgray', border: '1px solid gray' }} > <DismissButton onDismiss={state.close} /> {children} <DismissButton onDismiss={state.close} /> </div> </Overlay> ); } import { DismissButton, Overlay, usePopover } from 'react-aria'; import type {AriaPopoverProps} from 'react-aria'; import type {OverlayTriggerState} from 'react-stately'; interface PopoverProps extends Omit<AriaPopoverProps, 'popoverRef'> { children: React.ReactNode; state: OverlayTriggerState; } function Popover( { children, state, ...props }: PopoverProps ) { let popoverRef = React.useRef(null); let { popoverProps, underlayProps } = usePopover({ ...props, popoverRef }, state); return ( <Overlay> <div {...underlayProps} style={{ position: 'fixed', inset: 0 }} /> <div {...popoverProps} ref={popoverRef} style={{ ...popoverProps.style, background: 'lightgray', border: '1px solid gray' }} > <DismissButton onDismiss={state.close} /> {children} <DismissButton onDismiss={state.close} /> </div> </Overlay> ); } import { DismissButton, Overlay, usePopover } from 'react-aria'; import type {AriaPopoverProps} from 'react-aria'; import type {OverlayTriggerState} from 'react-stately'; interface PopoverProps extends Omit< AriaPopoverProps, 'popoverRef' > { children: React.ReactNode; state: OverlayTriggerState; } function Popover( { children, state, ...props }: PopoverProps ) { let popoverRef = React .useRef(null); let { popoverProps, underlayProps } = usePopover({ ...props, popoverRef }, state); return ( <Overlay> <div {...underlayProps} style={{ position: 'fixed', inset: 0 }} /> <div {...popoverProps} ref={popoverRef} style={{ ...popoverProps .style, background: 'lightgray', border: '1px solid gray' }} > <DismissButton onDismiss={state .close} /> {children} <DismissButton onDismiss={state .close} /> </div> </Overlay> ); } ### Button# The `Button` component is used in the above example to toggle the menu. It is built using the useButton hook, and can be shared with many other components. Show code import {useButton} from 'react-aria'; function Button(props) { let ref = props.buttonRef; let { buttonProps } = useButton(props, ref); return ( <button {...buttonProps} ref={ref} style={props.style}> {props.children} </button> ); } import {useButton} from 'react-aria'; function Button(props) { let ref = props.buttonRef; let { buttonProps } = useButton(props, ref); return ( <button {...buttonProps} ref={ref} style={props.style}> {props.children} </button> ); } import {useButton} from 'react-aria'; function Button(props) { let ref = props.buttonRef; let { buttonProps } = useButton( props, ref ); return ( <button {...buttonProps} ref={ref} style={props.style} > {props.children} </button> ); } ## Styled examples# * * * Tailwind CSS An example of styling a Menu with Tailwind. ## Dynamic collections# * * * `Menu` follows the Collection Components API, accepting both static and dynamic collections. The examples above show static collections, which can be used when the full list of options is known ahead of time. Dynamic collections, as shown below, can be used when the options come from an external data source such as an API call, or update over time. As seen below, an iterable list of options is passed to the ComboBox using the `items` prop. Each item accepts a `key` prop, which is passed to the `onSelectionChange` handler to identify the selected item. Alternatively, if the item objects contain an `id` property, as shown in the example below, then this is used automatically and a `key` prop is not required. function Example() { let items = [ {id: 1, name: 'New'}, {id: 2, name: 'Open'}, {id: 3, name: 'Close'}, {id: 4, name: 'Save'}, {id: 5, name: 'Duplicate'}, {id: 6, name: 'Rename'}, {id: 7, name: 'Move'} ]; return ( <MenuButton label="Actions" items={items} onAction={alert}> {(item) => <Item>{item.name}</Item>} </MenuButton> ); } function Example() { let items = [ { id: 1, name: 'New' }, { id: 2, name: 'Open' }, { id: 3, name: 'Close' }, { id: 4, name: 'Save' }, { id: 5, name: 'Duplicate' }, { id: 6, name: 'Rename' }, { id: 7, name: 'Move' } ]; return ( <MenuButton label="Actions" items={items} onAction={alert} > {(item) => <Item>{item.name}</Item>} </MenuButton> ); } function Example() { let items = [ { id: 1, name: 'New' }, { id: 2, name: 'Open' }, { id: 3, name: 'Close' }, { id: 4, name: 'Save' }, { id: 5, name: 'Duplicate' }, { id: 6, name: 'Rename' }, { id: 7, name: 'Move' } ]; return ( <MenuButton label="Actions" items={items} onAction={alert} > {(item) => ( <Item> {item.name} </Item> )} </MenuButton> ); } Actions▼ ## Selection# * * * Menu supports multiple selection modes. By default, selection is disabled, however this can be changed using the `selectionMode` prop. Use `defaultSelectedKeys` to provide a default set of selected items (uncontrolled) and `selectedKeys` to set the selected items (controlled). The value of the selected keys must match the `key` prop of the items. See the `react-stately` Selection docs for more details. import type {Selection} from 'react-stately'; function Example() { let [selected, setSelected] = React.useState<Selection>( new Set(['sidebar', 'console']) ); return ( <> <MenuButton label="View" selectionMode="multiple" selectedKeys={selected} onSelectionChange={setSelected} > <Item key="sidebar">Sidebar</Item> <Item key="searchbar">Searchbar</Item> <Item key="tools">Tools</Item> <Item key="console">Console</Item> </MenuButton> <p> Current selection (controlled):{' '} {selected === 'all' ? 'all' : [...selected].join(', ')} </p> </> ); } import type {Selection} from 'react-stately'; function Example() { let [selected, setSelected] = React.useState<Selection>( new Set(['sidebar', 'console']) ); return ( <> <MenuButton label="View" selectionMode="multiple" selectedKeys={selected} onSelectionChange={setSelected} > <Item key="sidebar">Sidebar</Item> <Item key="searchbar">Searchbar</Item> <Item key="tools">Tools</Item> <Item key="console">Console</Item> </MenuButton> <p> Current selection (controlled): {selected === 'all' ? 'all' : [...selected].join(', ')} </p> </> ); } import type {Selection} from 'react-stately'; function Example() { let [ selected, setSelected ] = React.useState< Selection >( new Set([ 'sidebar', 'console' ]) ); return ( <> <MenuButton label="View" selectionMode="multiple" selectedKeys={selected} onSelectionChange={setSelected} > <Item key="sidebar"> Sidebar </Item> <Item key="searchbar"> Searchbar </Item> <Item key="tools"> Tools </Item> <Item key="console"> Console </Item> </MenuButton> <p> Current selection (controlled): {' '} {selected === 'all' ? 'all' : [...selected] .join(', ')} </p> </> ); } View▼ Current selection (controlled): sidebar, console ## Sections# * * * Menu supports sections with separators and headings in order to group options. Sections can be used by wrapping groups of Items in a `Section` component. Each `Section` takes a `title` and `key` prop. To implement sections, implement the `ListBoxSection` component referenced above using the `useMenuSection` hook. It will include four extra elements: an `<li>` between the sections to represent the separator, an `<li>` to contain the heading `<span>` element, and a `<ul>` to contain the child items. This structure is necessary to ensure HTML semantics are correct. import {useMenuSection, useSeparator} from 'react-aria'; function MenuSection({ section, state }) { let { itemProps, headingProps, groupProps } = useMenuSection({ heading: section.rendered, 'aria-label': section['aria-label'] }); let { separatorProps } = useSeparator({ elementType: 'li' }); // If the section is not the first, add a separator element. // The heading is rendered inside an <li> element, which contains // a <ul> with the child items. return ( <> {section.key !== state.collection.getFirstKey() && ( <li {...separatorProps} style={{ borderTop: '1px solid gray', margin: '2px 5px' }} /> )} <li {...itemProps}> {section.rendered && ( <span {...headingProps} style={{ fontWeight: 'bold', fontSize: '1.1em', padding: '2px 5px' }} > {section.rendered} </span> )} <ul {...groupProps} style={{ padding: 0, listStyle: 'none' }} > {[...section.childNodes].map((node) => ( <MenuItem key={node.key} item={node} state={state} /> ))} </ul> </li> </> ); } import {useMenuSection, useSeparator} from 'react-aria'; function MenuSection({ section, state }) { let { itemProps, headingProps, groupProps } = useMenuSection({ heading: section.rendered, 'aria-label': section['aria-label'] }); let { separatorProps } = useSeparator({ elementType: 'li' }); // If the section is not the first, add a separator element. // The heading is rendered inside an <li> element, which contains // a <ul> with the child items. return ( <> {section.key !== state.collection.getFirstKey() && ( <li {...separatorProps} style={{ borderTop: '1px solid gray', margin: '2px 5px' }} /> )} <li {...itemProps}> {section.rendered && ( <span {...headingProps} style={{ fontWeight: 'bold', fontSize: '1.1em', padding: '2px 5px' }} > {section.rendered} </span> )} <ul {...groupProps} style={{ padding: 0, listStyle: 'none' }} > {[...section.childNodes].map((node) => ( <MenuItem key={node.key} item={node} state={state} /> ))} </ul> </li> </> ); } import { useMenuSection, useSeparator } from 'react-aria'; function MenuSection( { section, state } ) { let { itemProps, headingProps, groupProps } = useMenuSection({ heading: section.rendered, 'aria-label': section[ 'aria-label' ] }); let { separatorProps } = useSeparator({ elementType: 'li' }); // If the section is not the first, add a separator element. // The heading is rendered inside an <li> element, which contains // a <ul> with the child items. return ( <> {section.key !== state .collection .getFirstKey() && ( <li {...separatorProps} style={{ borderTop: '1px solid gray', margin: '2px 5px' }} /> )} <li {...itemProps}> {section .rendered && ( <span {...headingProps} style={{ fontWeight: 'bold', fontSize: '1.1em', padding: '2px 5px' }} > {section .rendered} </span> )} <ul {...groupProps} style={{ padding: 0, listStyle: 'none' }} > {[ ...section .childNodes ].map( (node) => ( <MenuItem key={node .key} item={node} state={state} /> ) )} </ul> </li> </> ); } ### Static items# With this in place, we can now render a static menu with multiple sections: import {Section} from 'react-stately'; <MenuButton label="Actions" onAction={alert}> <Section title="Styles"> <Item key="bold">Bold</Item> <Item key="underline">Underline</Item> </Section> <Section title="Align"> <Item key="left">Left</Item> <Item key="middle">Middle</Item> <Item key="right">Right</Item> </Section> </MenuButton> import {Section} from 'react-stately'; <MenuButton label="Actions" onAction={alert}> <Section title="Styles"> <Item key="bold">Bold</Item> <Item key="underline">Underline</Item> </Section> <Section title="Align"> <Item key="left">Left</Item> <Item key="middle">Middle</Item> <Item key="right">Right</Item> </Section> </MenuButton> import {Section} from 'react-stately'; <MenuButton label="Actions" onAction={alert} > <Section title="Styles"> <Item key="bold"> Bold </Item> <Item key="underline"> Underline </Item> </Section> <Section title="Align"> <Item key="left"> Left </Item> <Item key="middle"> Middle </Item> <Item key="right"> Right </Item> </Section> </MenuButton> Actions▼ ### Dynamic items# The above example shows sections with static items. Sections can also be populated from a hierarchical data structure. Similarly to the props on Menu, `<Section>` takes an array of data using the `items` prop. import type {Selection} from 'react-stately'; function Example() { let [selected, setSelected] = React.useState<Selection>(new Set([1,3])); let openWindows = [ { name: 'Left Panel', id: 'left', children: [ {id: 1, name: 'Final Copy (1)'} ] }, { name: 'Right Panel', id: 'right', children: [ {id: 2, name: 'index.ts'}, {id: 3, name: 'package.json'}, {id: 4, name: 'license.txt'} ] } ]; return ( <MenuButton label="Window" items={openWindows} selectionMode="multiple" selectedKeys={selected} onSelectionChange={setSelected}> {item => ( <Section items={item.children} title={item.name}> {item => <Item>{item.name}</Item>} </Section> )} </MenuButton> ); } import type {Selection} from 'react-stately'; function Example() { let [selected, setSelected] = React.useState<Selection>( new Set([1, 3]) ); let openWindows = [ { name: 'Left Panel', id: 'left', children: [ { id: 1, name: 'Final Copy (1)' } ] }, { name: 'Right Panel', id: 'right', children: [ { id: 2, name: 'index.ts' }, { id: 3, name: 'package.json' }, { id: 4, name: 'license.txt' } ] } ]; return ( <MenuButton label="Window" items={openWindows} selectionMode="multiple" selectedKeys={selected} onSelectionChange={setSelected} > {(item) => ( <Section items={item.children} title={item.name}> {(item) => <Item>{item.name}</Item>} </Section> )} </MenuButton> ); } import type {Selection} from 'react-stately'; function Example() { let [ selected, setSelected ] = React.useState< Selection >(new Set([1, 3])); let openWindows = [ { name: 'Left Panel', id: 'left', children: [ { id: 1, name: 'Final Copy (1)' } ] }, { name: 'Right Panel', id: 'right', children: [ { id: 2, name: 'index.ts' }, { id: 3, name: 'package.json' }, { id: 4, name: 'license.txt' } ] } ]; return ( <MenuButton label="Window" items={openWindows} selectionMode="multiple" selectedKeys={selected} onSelectionChange={setSelected} > {(item) => ( <Section items={item .children} title={item .name} > {(item) => ( <Item> {item.name} </Item> )} </Section> )} </MenuButton> ); } Window▼ ### Accessibility# Sections without a `title` must provide an `aria-label` for accessibility. ## Complex menu items# * * * By default, menu items that only contain text will be labeled by the contents of the item. For items that have more complex content (e.g. icons, multiple lines of text, keyboard shortcuts, etc.), use `labelProps`, `descriptionProps`, and `keyboardShortcutProps` from `useMenuItem` as needed to apply to the main text element of the menu item, its description, and keyboard shortcut text. This improves screen reader announcement. **NOTE: menu items cannot contain interactive content (e.g. buttons, checkboxes, etc.).** To implement this, we'll update the `MenuItem` component to apply the ARIA properties returned by `useMenuItem` to the appropriate elements. In this example, we'll pull them out of `props.children` and use `React.cloneElement` to apply the props, but you may want to use a more robust approach (e.g. context). function MenuItem({item, state}) { // Get props for the menu item element and child elements let ref = React.useRef(null); let { menuItemProps, labelProps, descriptionProps, keyboardShortcutProps } = useMenuItem({key: item.key}, state, ref); // Pull out the three expected children. We will clone them // and add the necessary props for accessibility. let [title, description, shortcut] = item.rendered; return ( <li {...menuItemProps} ref={ref}> <div> {React.cloneElement(title, labelProps)} {React.cloneElement(description, descriptionProps)} </div> {React.cloneElement(shortcut, keyboardShortcutProps)} </li> ); } <MenuButton label="Actions" onAction={alert}> <Item textValue="Copy" key="copy"> <div><strong>Copy</strong></div> <div>Copy the selected text</div> <kbd>⌘C</kbd> </Item> <Item textValue="Cut" key="cut"> <div><strong>Cut</strong></div> <div>Cut the selected text</div> <kbd>⌘X</kbd> </Item> <Item textValue="Paste" key="paste"> <div><strong>Paste</strong></div> <div>Paste the copied text</div> <kbd>⌘V</kbd> </Item> </MenuButton> function MenuItem({ item, state }) { // Get props for the menu item element and child elements let ref = React.useRef(null); let { menuItemProps, labelProps, descriptionProps, keyboardShortcutProps } = useMenuItem({ key: item.key }, state, ref); // Pull out the three expected children. We will clone them // and add the necessary props for accessibility. let [title, description, shortcut] = item.rendered; return ( <li {...menuItemProps} ref={ref}> <div> {React.cloneElement(title, labelProps)} {React.cloneElement(description, descriptionProps)} </div> {React.cloneElement(shortcut, keyboardShortcutProps)} </li> ); } <MenuButton label="Actions" onAction={alert}> <Item textValue="Copy" key="copy"> <div> <strong>Copy</strong> </div> <div>Copy the selected text</div> <kbd>⌘C</kbd> </Item> <Item textValue="Cut" key="cut"> <div> <strong>Cut</strong> </div> <div>Cut the selected text</div> <kbd>⌘X</kbd> </Item> <Item textValue="Paste" key="paste"> <div> <strong>Paste</strong> </div> <div>Paste the copied text</div> <kbd>⌘V</kbd> </Item> </MenuButton> function MenuItem( { item, state } ) { // Get props for the menu item element and child elements let ref = React.useRef( null ); let { menuItemProps, labelProps, descriptionProps, keyboardShortcutProps } = useMenuItem( { key: item.key }, state, ref ); // Pull out the three expected children. We will clone them // and add the necessary props for accessibility. let [ title, description, shortcut ] = item.rendered; return ( <li {...menuItemProps} ref={ref} > <div> {React .cloneElement( title, labelProps )} {React .cloneElement( description, descriptionProps )} </div> {React .cloneElement( shortcut, keyboardShortcutProps )} </li> ); } <MenuButton label="Actions" onAction={alert} > <Item textValue="Copy" key="copy" > <div> <strong> Copy </strong> </div> <div> Copy the selected text </div> <kbd>⌘C</kbd> </Item> <Item textValue="Cut" key="cut" > <div> <strong> Cut </strong> </div> <div> Cut the selected text </div> <kbd>⌘X</kbd> </Item> <Item textValue="Paste" key="paste" > <div> <strong> Paste </strong> </div> <div> Paste the copied text </div> <kbd>⌘V</kbd> </Item> </MenuButton> Actions▼ ## Disabled items# * * * `useMenu` supports marking items as disabled using the `disabledKeys` prop. Each key in this list corresponds with the `key` prop passed to the `Item` component, or automatically derived from the values passed to the `items` prop. See Collections for more details. Disabled items are not focusable or keyboard navigable, and do not trigger `onAction` or `onSelectionChange`. The `isDisabled` property returned by `useMenuItem` can be used to style the item appropriately. <MenuButton label="Actions" onAction={alert} disabledKeys={['paste']}> <Item key="copy">Copy</Item> <Item key="cut">Cut</Item> <Item key="paste">Paste</Item> </MenuButton> <MenuButton label="Actions" onAction={alert} disabledKeys={['paste']} > <Item key="copy">Copy</Item> <Item key="cut">Cut</Item> <Item key="paste">Paste</Item> </MenuButton> <MenuButton label="Actions" onAction={alert} disabledKeys={[ 'paste' ]} > <Item key="copy"> Copy </Item> <Item key="cut"> Cut </Item> <Item key="paste"> Paste </Item> </MenuButton> Actions▼ ## Links# * * * By default, interacting with an item in a Menu triggers `onAction` and optionally `onSelectionChange` depending on the `selectionMode`. Alternatively, items may be links to another page or website. This can be achieved by passing the `href` prop to the `<Item>` component. Link items in a menu are not selectable. This example shows how to update the `MenuItem` component with support for rendering an `<a>` element if an `href` prop is passed to the item. Note that you'll also need to render the `Menu` as a `<div>` instead of a `<ul>`, since an `<a>` inside a `<ul>` is not valid HTML. function MenuItem({item, state}) { // Get props for the menu item element and child elements let ref = React.useRef(null); let {menuItemProps} = useMenuItem({key: item.key}, state, ref); let ElementType: React.ElementType = item.props.href ? 'a' : 'div'; return ( <ElementType {...menuItemProps} ref={ref}> {item.rendered} </ElementType> ); } <MenuButton label="Links"> <Item href="https://adobe.com/" target="_blank">Adobe</Item> <Item href="https://apple.com/" target="_blank">Apple</Item> <Item href="https://google.com/" target="_blank">Google</Item> <Item href="https://microsoft.com/" target="_blank">Microsoft</Item> </MenuButton> function MenuItem({ item, state }) { // Get props for the menu item element and child elements let ref = React.useRef(null); let { menuItemProps } = useMenuItem( { key: item.key }, state, ref ); let ElementType: React.ElementType = item.props.href ? 'a' : 'div'; return ( <ElementType {...menuItemProps} ref={ref}> {item.rendered} </ElementType> ); } <MenuButton label="Links"> <Item href="https://adobe.com/" target="_blank"> Adobe </Item> <Item href="https://apple.com/" target="_blank"> Apple </Item> <Item href="https://google.com/" target="_blank"> Google </Item> <Item href="https://microsoft.com/" target="_blank"> Microsoft </Item> </MenuButton> function MenuItem( { item, state } ) { // Get props for the menu item element and child elements let ref = React.useRef( null ); let { menuItemProps } = useMenuItem( { key: item.key }, state, ref ); let ElementType: React.ElementType = item.props.href ? 'a' : 'div'; return ( <ElementType {...menuItemProps} ref={ref} > {item.rendered} </ElementType> ); } <MenuButton label="Links"> <Item href="https://adobe.com/" target="_blank" > Adobe </Item> <Item href="https://apple.com/" target="_blank" > Apple </Item> <Item href="https://google.com/" target="_blank" > Google </Item> <Item href="https://microsoft.com/" target="_blank" > Microsoft </Item> </MenuButton> Links▼ ### Client side routing# The `<Item>` component works with frameworks and client side routers like Next.js and React Router. As with other React Aria components that support links, this works via the `RouterProvider` component at the root of your app. See the client side routing guide to learn how to set this up. ## Controlled open state# * * * The open state of the menu can be controlled via the `defaultOpen` and `isOpen` props function Example() { let [open, setOpen] = React.useState(false); return ( <> <p>Menu is {open ? 'open' : 'closed'}</p> <MenuButton label="View" isOpen={open} onOpenChange={setOpen}> <Item key="side">Side bar</Item> <Item key="options">Page options</Item> <Item key="edit">Edit Panel</Item> </MenuButton> </> ); } function Example() { let [open, setOpen] = React.useState(false); return ( <> <p>Menu is {open ? 'open' : 'closed'}</p> <MenuButton label="View" isOpen={open} onOpenChange={setOpen}> <Item key="side">Side bar</Item> <Item key="options">Page options</Item> <Item key="edit">Edit Panel</Item> </MenuButton> </> ); } function Example() { let [open, setOpen] = React.useState( false ); return ( <> <p> Menu is {open ? 'open' : 'closed'} </p> <MenuButton label="View" isOpen={open} onOpenChange={setOpen} > <Item key="side"> Side bar </Item> <Item key="options"> Page options </Item> <Item key="edit"> Edit Panel </Item> </MenuButton> </> ); } Menu is closed View▼ ## Internationalization# * * * `useMenu` handles some aspects of internationalization automatically. For example, type to select is implemented with an Intl.Collator for internationalized string matching. You are responsible for localizing all menu item labels for content that is passed into the menu. ### RTL# In right-to-left languages, the menu button should be mirrored. The arrow should be on the left, and the label should be on the right. In addition, the content of menu items should flip. Ensure that your CSS accounts for this. --- ## Page: https://react-spectrum.adobe.com/react-aria/useTable.html # useTable Provides the behavior and accessibility implementation for a table component. A table displays data in rows and columns and enables a user to navigate its contents via directional navigation keys, and optionally supports row selection and sorting. <table><tbody><tr><th>install</th><td><code>yarn add react-aria</code></td></tr><tr><th>version</th><td>3.41.1</td></tr><tr><th>usage</th><td><code><span>import</span> {useTable, useTableCell, useTableColumnHeader, useTableRow, useTableHeaderRow, useTableRowGroup, useTableSelectAllCheckbox, useTableSelectionCheckbox, useTableColumnResize} <span>from</span> <span>'react-aria'</span></code></td></tr></tbody></table> View ARIA pattern W3C View repository GitHub View package NPM ## API# * * * `useTable<T>( props: AriaTableProps , state: TableState <T> | | TreeGridState <T>, ref: RefObject <HTMLElement | | null> ): GridAria ``useTableRowGroup(): GridRowGroupAria ``useTableHeaderRow<T>( props: GridRowProps <T>, state: TableState <T>, ref: RefObject <Element | | null> ): TableHeaderRowAria ``useTableColumnHeader<T>( props: AriaTableColumnHeaderProps <T>, state: TableState <T>, ref: RefObject <FocusableElement | | null> ): TableColumnHeaderAria ``useTableRow<T>( props: GridRowProps <T>, state: TableState <T> | | TreeGridState <T>, ref: RefObject <FocusableElement | | null> ): GridRowAria ``useTableCell<T>( props: AriaTableCellProps , state: TableState <T>, ref: RefObject <FocusableElement | | null> ): TableCellAria ``useTableSelectionCheckbox<T>( (props: AriaTableSelectionCheckboxProps , , state: TableState <T> )): TableSelectionCheckboxAria ``useTableSelectAllCheckbox<T>( (state: TableState <T> )): TableSelectAllCheckboxAria ``useTableColumnResize<T>( props: AriaTableColumnResizeProps <T>, state: TableColumnResizeState <T>, ref: RefObject <HTMLInputElement | | null> ): TableColumnResizeAria ` ## Features# * * * A table can be built using the <table>, <tr>, <td>, and other table specific HTML elements, but is very limited in functionality especially when it comes to user interactions. HTML tables are meant for static content, rather than tables with rich interactions like focusable elements within cells, keyboard navigation, row selection, sorting, etc. `useTable` helps achieve accessible and interactive table components that can be styled as needed. * Exposed to assistive technology as a `grid` using ARIA * Keyboard navigation between columns, rows, cells, and in-cell focusable elements via the arrow keys * Single, multiple, or no row selection via mouse, touch, or keyboard interactions * Support for disabled rows, which cannot be selected * Optional support for checkboxes in each row for selection, as well as in the header to select all rows * Support for both `toggle` and `replace` selection behaviors * Support for row actions via double click, Enter key, or tapping * Long press to enter selection mode on touch when there is both selection and row actions * Column sorting support * Async loading, infinite scrolling, filtering, and sorting support * Support for column groups via nested columns * Typeahead to allow focusing rows by typing text * Automatic scrolling support during keyboard navigation * Labeling support for accessibility * Support for marking columns as row headers, which will be read when navigating the rows with a screen reader * Ensures that selections are announced using an ARIA live region * Support for using HTML table elements, or custom element types (e.g. `<div>`) for layout flexibility * Support for use with virtualized lists * Support for resizable columns ## Anatomy# * * * A table consists of a container element, with columns and rows of cells containing data inside. The cells within a table may contain focusable elements or plain text content. If the table supports row selection, each row can optionally include a selection checkbox in the first column. Additionally, a "select all" checkbox is displayed as the first column header if the table supports multiple row selection. The `useTable` ,` useTableRow `,` useTableCell `, and` useTableColumnHeader `hooks handle keyboard, mouse, and other interactions to support row selection, in table navigation, and overall focus behavior. Those hooks, along with` useTableRowGroup `and` useTableHeaderRow `, also handle exposing the table and its contents to assistive technology using ARIA.` useTableSelectAllCheckbox `and` useTableSelectionCheckbox `handle row selection and associating each checkbox with its respective rows for assistive technology. Each of these hooks returns props to be spread onto the appropriate HTML element. State is managed by the `useTableState` hook from `@react-stately/table`. The state object should be passed as an option to each of the above hooks where applicable. Note that an `aria-label` or `aria-labelledby` must be passed to the table to identify the element to assistive technology. ## State management# * * * `useTable` requires knowledge of the rows, cells, and columns in the table in order to handle keyboard navigation and other interactions. It does this using the `Collection` interface, which is a generic interface to access sequential unique keyed data. You can implement this interface yourself, e.g. by using a prop to pass a list of item objects, but` useTableState `from `@react-stately/table` implements a JSX based interface for building collections instead. See Collection Components for more information, and Collection Interface for internal details. Data is defined using the `TableHeader` ,` Column `,` TableBody `,` Row `, and` Cell `components, which support both static and dynamic data. See the examples in the usage section below for details on how to use these components. In addition, `useTableState` manages the state necessary for multiple selection and exposes a` SelectionManager `, which makes use of the collection to provide an interface to update the selection state. For more information, see Selection. ## Example# * * * Tables are complex collection components that are built up from many child elements including columns, rows, and cells. In this example, we'll use the standard HTML table elements along with hooks from React Aria for each child. You may also use other elements like `<div>` to render these components as appropriate. Since there are many pieces, we'll walk through each of them one by one. The `useTable` hook will be used to render the outer most table element. It uses the` useTableState `hook to construct the table's collection of rows and columns, and manage state such as the focused row/cell, selection, and sort column/direction. We'll use the collection to iterate through the rows and cells of the table and render the relevant components, which we'll define below. import {mergeProps, useFocusRing, useTable} from 'react-aria'; import {Cell, Column, Row, TableBody, TableHeader, useTableState} from 'react-stately'; import {useRef} from 'react'; function Table(props) { let { selectionMode, selectionBehavior } = props; let state = useTableState({ ...props, showSelectionCheckboxes: selectionMode === 'multiple' && selectionBehavior !== 'replace' }); let ref = useRef<HTMLTableElement | null>(null); let { collection } = state; let { gridProps } = useTable(props, state, ref); return ( <table {...gridProps} ref={ref} style={{ borderCollapse: 'collapse' }}> <TableRowGroup type="thead"> {collection.headerRows.map((headerRow) => ( <TableHeaderRow key={headerRow.key} item={headerRow} state={state}> {[...headerRow.childNodes].map((column) => column.props.isSelectionCell ? ( <TableSelectAllCell key={column.key} column={column} state={state} /> ) : ( <TableColumnHeader key={column.key} column={column} state={state} /> ) )} </TableHeaderRow> ))} </TableRowGroup> <TableRowGroup type="tbody"> {[...collection.body.childNodes].map((row) => ( <TableRow key={row.key} item={row} state={state}> {[...row.childNodes].map((cell) => cell.props.isSelectionCell ? <TableCheckboxCell key={cell.key} cell={cell} state={state} /> : <TableCell key={cell.key} cell={cell} state={state} /> )} </TableRow> ))} </TableRowGroup> </table> ); } import { mergeProps, useFocusRing, useTable } from 'react-aria'; import { Cell, Column, Row, TableBody, TableHeader, useTableState } from 'react-stately'; import {useRef} from 'react'; function Table(props) { let { selectionMode, selectionBehavior } = props; let state = useTableState({ ...props, showSelectionCheckboxes: selectionMode === 'multiple' && selectionBehavior !== 'replace' }); let ref = useRef<HTMLTableElement | null>(null); let { collection } = state; let { gridProps } = useTable(props, state, ref); return ( <table {...gridProps} ref={ref} style={{ borderCollapse: 'collapse' }} > <TableRowGroup type="thead"> {collection.headerRows.map((headerRow) => ( <TableHeaderRow key={headerRow.key} item={headerRow} state={state} > {[...headerRow.childNodes].map((column) => column.props.isSelectionCell ? ( <TableSelectAllCell key={column.key} column={column} state={state} /> ) : ( <TableColumnHeader key={column.key} column={column} state={state} /> ) )} </TableHeaderRow> ))} </TableRowGroup> <TableRowGroup type="tbody"> {[...collection.body.childNodes].map((row) => ( <TableRow key={row.key} item={row} state={state}> {[...row.childNodes].map((cell) => cell.props.isSelectionCell ? ( <TableCheckboxCell key={cell.key} cell={cell} state={state} /> ) : ( <TableCell key={cell.key} cell={cell} state={state} /> ) )} </TableRow> ))} </TableRowGroup> </table> ); } import { mergeProps, useFocusRing, useTable } from 'react-aria'; import { Cell, Column, Row, TableBody, TableHeader, useTableState } from 'react-stately'; import {useRef} from 'react'; function Table(props) { let { selectionMode, selectionBehavior } = props; let state = useTableState({ ...props, showSelectionCheckboxes: selectionMode === 'multiple' && selectionBehavior !== 'replace' }); let ref = useRef< | HTMLTableElement | null >(null); let { collection } = state; let { gridProps } = useTable( props, state, ref ); return ( <table {...gridProps} ref={ref} style={{ borderCollapse: 'collapse' }} > <TableRowGroup type="thead"> {collection .headerRows .map( (headerRow) => ( <TableHeaderRow key={headerRow .key} item={headerRow} state={state} > {[ ...headerRow .childNodes ].map( (column) => column .props .isSelectionCell ? ( <TableSelectAllCell key={column .key} column={column} state={state} /> ) : ( <TableColumnHeader key={column .key} column={column} state={state} /> ) )} </TableHeaderRow> ) )} </TableRowGroup> <TableRowGroup type="tbody"> {[ ...collection .body .childNodes ].map((row) => ( <TableRow key={row.key} item={row} state={state} > {[ ...row .childNodes ].map( (cell) => cell .props .isSelectionCell ? ( <TableCheckboxCell key={cell .key} cell={cell} state={state} /> ) : ( <TableCell key={cell .key} cell={cell} state={state} /> ) )} </TableRow> ))} </TableRowGroup> </table> ); } ### Table header# A `useTableRowGroup` hook will be used to group the rows in the table header and table body. In this example, we're using HTML table elements, so this will be either a `<thead>` or `<tbody>` element, as passed from the above `Table` component via the `type` prop. import {useTableRowGroup} from 'react-aria'; function TableRowGroup({ type: Element, children }) { let { rowGroupProps } = useTableRowGroup(); return ( <Element {...rowGroupProps} style={Element === 'thead' ? { borderBottom: '2px solid var(--spectrum-global-color-gray-800)' } : null} > {children} </Element> ); } import {useTableRowGroup} from 'react-aria'; function TableRowGroup({ type: Element, children }) { let { rowGroupProps } = useTableRowGroup(); return ( <Element {...rowGroupProps} style={Element === 'thead' ? { borderBottom: '2px solid var(--spectrum-global-color-gray-800)' } : null} > {children} </Element> ); } import {useTableRowGroup} from 'react-aria'; function TableRowGroup( { type: Element, children } ) { let { rowGroupProps } = useTableRowGroup(); return ( <Element {...rowGroupProps} style={Element === 'thead' ? { borderBottom: '2px solid var(--spectrum-global-color-gray-800)' } : null} > {children} </Element> ); } The `useTableHeaderRow` hook will be used to render a header row. Header rows are similar to other rows, but they don't support user interaction like selection. In this example, there's only one header row, but there could be multiple in the case of nested columns. See the example below for details. import {useTableHeaderRow} from 'react-aria'; function TableHeaderRow({ item, state, children }) { let ref = useRef<HTMLTableRowElement | null>(null); let { rowProps } = useTableHeaderRow({ node: item }, state, ref); return ( <tr {...rowProps} ref={ref}> {children} </tr> ); } import {useTableHeaderRow} from 'react-aria'; function TableHeaderRow({ item, state, children }) { let ref = useRef<HTMLTableRowElement | null>(null); let { rowProps } = useTableHeaderRow( { node: item }, state, ref ); return ( <tr {...rowProps} ref={ref}> {children} </tr> ); } import {useTableHeaderRow} from 'react-aria'; function TableHeaderRow( { item, state, children } ) { let ref = useRef< | HTMLTableRowElement | null >(null); let { rowProps } = useTableHeaderRow( { node: item }, state, ref ); return ( <tr {...rowProps} ref={ref} > {children} </tr> ); } The `useTableColumnHeader` hook will be used to render each column header. Column headers act as a label for all of the cells in that column, and can optionally support user interaction to sort by the column and change the sort order. The `allowsSorting` property of the column object can be used to determine if the column supports sorting at all. The `sortDescriptor` object stored in the `state` object indicates which column the table is currently sorted by, as well as the sort direction (ascending or descending). This is used to render an arrow icon to visually indicate the sort direction. When not sorted by this column, we use `visibility: hidden` to ensure that we reserve space for this icon at all times. That way the table's layout doesn't shift when we change the column we're sorting by. See the example below of all of this in action. Finally, we use the `useFocusRing` hook to ensure that a focus ring is rendered when the cell is navigated to with the keyboard. import {useTableColumnHeader} from 'react-aria'; function TableColumnHeader({ column, state }) { let ref = useRef<HTMLTableCellElement | null>(null); let { columnHeaderProps } = useTableColumnHeader( { node: column }, state, ref ); let { isFocusVisible, focusProps } = useFocusRing(); let arrowIcon = state.sortDescriptor?.direction === 'ascending' ? '▲' : '▼'; return ( <th {...mergeProps(columnHeaderProps, focusProps)} style={{ textAlign: column.colSpan > 1 ? 'center' : 'left', padding: '5px 10px', outline: 'none', boxShadow: isFocusVisible ? 'inset 0 0 0 2px orange' : 'none', cursor: 'default' }} ref={ref} > {column.rendered} {column.props.allowsSorting && ( <span aria-hidden="true" style={{ padding: '0 2px', visibility: state.sortDescriptor?.column === column.key ? 'visible' : 'hidden' }} > {arrowIcon} </span> )} </th> ); } import {useTableColumnHeader} from 'react-aria'; function TableColumnHeader({ column, state }) { let ref = useRef<HTMLTableCellElement | null>(null); let { columnHeaderProps } = useTableColumnHeader( { node: column }, state, ref ); let { isFocusVisible, focusProps } = useFocusRing(); let arrowIcon = state.sortDescriptor?.direction === 'ascending' ? '▲' : '▼'; return ( <th {...mergeProps(columnHeaderProps, focusProps)} style={{ textAlign: column.colSpan > 1 ? 'center' : 'left', padding: '5px 10px', outline: 'none', boxShadow: isFocusVisible ? 'inset 0 0 0 2px orange' : 'none', cursor: 'default' }} ref={ref} > {column.rendered} {column.props.allowsSorting && ( <span aria-hidden="true" style={{ padding: '0 2px', visibility: state.sortDescriptor?.column === column.key ? 'visible' : 'hidden' }} > {arrowIcon} </span> )} </th> ); } import {useTableColumnHeader} from 'react-aria'; function TableColumnHeader( { column, state } ) { let ref = useRef< | HTMLTableCellElement | null >(null); let { columnHeaderProps } = useTableColumnHeader( { node: column }, state, ref ); let { isFocusVisible, focusProps } = useFocusRing(); let arrowIcon = state.sortDescriptor ?.direction === 'ascending' ? '▲' : '▼'; return ( <th {...mergeProps( columnHeaderProps, focusProps )} style={{ textAlign: column .colSpan > 1 ? 'center' : 'left', padding: '5px 10px', outline: 'none', boxShadow: isFocusVisible ? 'inset 0 0 0 2px orange' : 'none', cursor: 'default' }} ref={ref} > {column.rendered} {column.props .allowsSorting && ( <span aria-hidden="true" style={{ padding: '0 2px', visibility: state .sortDescriptor ?.column === column .key ? 'visible' : 'hidden' }} > {arrowIcon} </span> )} </th> ); } ### Table body# Now that we've covered the table header, let's move on to the body. We'll use the `useTableRow` hook to render each row in the table. Table rows can be focused and navigated to using the keyboard via the arrow keys. In addition, table rows can optionally support selection via mouse, touch, or keyboard. Clicking, tapping, or pressing the Space key anywhere in the row selects it. Row actions are also supported, see below for details. We'll use the `SelectionManager` object exposed by the `state` to determine if a row is selected, and render a pink background if so. We'll also use the` useFocusRing `hook to render a focus ring when the user navigates to the row with the keyboard. import {useTableRow} from 'react-aria'; function TableRow({ item, children, state }) { let ref = useRef<HTMLTableRowElement | null>(null); let isSelected = state.selectionManager.isSelected(item.key); let { rowProps, isPressed } = useTableRow( { node: item }, state, ref ); let { isFocusVisible, focusProps } = useFocusRing(); return ( <tr style={{ background: isSelected ? 'blueviolet' : isPressed ? 'var(--spectrum-global-color-gray-400)' : item.index % 2 ? 'var(--spectrum-alias-highlight-hover)' : 'none', color: isSelected ? 'white' : null, outline: 'none', boxShadow: isFocusVisible ? 'inset 0 0 0 2px orange' : 'none', cursor: 'default' }} {...mergeProps(rowProps, focusProps)} ref={ref} > {children} </tr> ); } import {useTableRow} from 'react-aria'; function TableRow({ item, children, state }) { let ref = useRef<HTMLTableRowElement | null>(null); let isSelected = state.selectionManager.isSelected( item.key ); let { rowProps, isPressed } = useTableRow( { node: item }, state, ref ); let { isFocusVisible, focusProps } = useFocusRing(); return ( <tr style={{ background: isSelected ? 'blueviolet' : isPressed ? 'var(--spectrum-global-color-gray-400)' : item.index % 2 ? 'var(--spectrum-alias-highlight-hover)' : 'none', color: isSelected ? 'white' : null, outline: 'none', boxShadow: isFocusVisible ? 'inset 0 0 0 2px orange' : 'none', cursor: 'default' }} {...mergeProps(rowProps, focusProps)} ref={ref} > {children} </tr> ); } import {useTableRow} from 'react-aria'; function TableRow( { item, children, state } ) { let ref = useRef< | HTMLTableRowElement | null >(null); let isSelected = state .selectionManager .isSelected( item.key ); let { rowProps, isPressed } = useTableRow( { node: item }, state, ref ); let { isFocusVisible, focusProps } = useFocusRing(); return ( <tr style={{ background: isSelected ? 'blueviolet' : isPressed ? 'var(--spectrum-global-color-gray-400)' : item .index % 2 ? 'var(--spectrum-alias-highlight-hover)' : 'none', color: isSelected ? 'white' : null, outline: 'none', boxShadow: isFocusVisible ? 'inset 0 0 0 2px orange' : 'none', cursor: 'default' }} {...mergeProps( rowProps, focusProps )} ref={ref} > {children} </tr> ); } Finally, we'll use the `useTableCell` hook to render each cell. Users can use the left and right arrow keys to navigate to each cell in a row, as well as any focusable elements within a cell. This is indicated by the focus ring, as created with the` useFocusRing `hook. The cell's contents are available in the `rendered` property of the cell` Node `object. import {useTableCell} from 'react-aria'; function TableCell({ cell, state }) { let ref = useRef<HTMLTableCellElement | null>(null); let { gridCellProps } = useTableCell({ node: cell }, state, ref); let { isFocusVisible, focusProps } = useFocusRing(); return ( <td {...mergeProps(gridCellProps, focusProps)} style={{ padding: '5px 10px', outline: 'none', boxShadow: isFocusVisible ? 'inset 0 0 0 2px orange' : 'none' }} ref={ref} > {cell.rendered} </td> ); } import {useTableCell} from 'react-aria'; function TableCell({ cell, state }) { let ref = useRef<HTMLTableCellElement | null>(null); let { gridCellProps } = useTableCell( { node: cell }, state, ref ); let { isFocusVisible, focusProps } = useFocusRing(); return ( <td {...mergeProps(gridCellProps, focusProps)} style={{ padding: '5px 10px', outline: 'none', boxShadow: isFocusVisible ? 'inset 0 0 0 2px orange' : 'none' }} ref={ref} > {cell.rendered} </td> ); } import {useTableCell} from 'react-aria'; function TableCell( { cell, state } ) { let ref = useRef< | HTMLTableCellElement | null >(null); let { gridCellProps } = useTableCell( { node: cell }, state, ref ); let { isFocusVisible, focusProps } = useFocusRing(); return ( <td {...mergeProps( gridCellProps, focusProps )} style={{ padding: '5px 10px', outline: 'none', boxShadow: isFocusVisible ? 'inset 0 0 0 2px orange' : 'none' }} ref={ref} > {cell.rendered} </td> ); } With all of the above components in place, we can render an example of our Table in action. This example shows a static collection, where all of the data is hard coded. See below for examples of using this Table component with dynamic collections (e.g. from a server). Try tabbing into the table and navigating using the arrow keys. <Table aria-label="Example static collection table" style={{ height: '210px', maxWidth: '400px' }} > <TableHeader> <Column>Name</Column> <Column>Type</Column> <Column>Date Modified</Column> </TableHeader> <TableBody> <Row> <Cell>Games</Cell> <Cell>File folder</Cell> <Cell>6/7/2020</Cell> </Row> <Row> <Cell>Program Files</Cell> <Cell>File folder</Cell> <Cell>4/7/2021</Cell> </Row> <Row> <Cell>bootmgr</Cell> <Cell>System file</Cell> <Cell>11/20/2010</Cell> </Row> <Row> <Cell>log.txt</Cell> <Cell>Text Document</Cell> <Cell>1/18/2016</Cell> </Row> </TableBody> </Table> <Table aria-label="Example static collection table" style={{ height: '210px', maxWidth: '400px' }} > <TableHeader> <Column>Name</Column> <Column>Type</Column> <Column>Date Modified</Column> </TableHeader> <TableBody> <Row> <Cell>Games</Cell> <Cell>File folder</Cell> <Cell>6/7/2020</Cell> </Row> <Row> <Cell>Program Files</Cell> <Cell>File folder</Cell> <Cell>4/7/2021</Cell> </Row> <Row> <Cell>bootmgr</Cell> <Cell>System file</Cell> <Cell>11/20/2010</Cell> </Row> <Row> <Cell>log.txt</Cell> <Cell>Text Document</Cell> <Cell>1/18/2016</Cell> </Row> </TableBody> </Table> <Table aria-label="Example static collection table" style={{ height: '210px', maxWidth: '400px' }} > <TableHeader> <Column> Name </Column> <Column> Type </Column> <Column> Date Modified </Column> </TableHeader> <TableBody> <Row> <Cell> Games </Cell> <Cell> File folder </Cell> <Cell> 6/7/2020 </Cell> </Row> <Row> <Cell> Program Files </Cell> <Cell> File folder </Cell> <Cell> 4/7/2021 </Cell> </Row> <Row> <Cell> bootmgr </Cell> <Cell> System file </Cell> <Cell> 11/20/2010 </Cell> </Row> <Row> <Cell> log.txt </Cell> <Cell> Text Document </Cell> <Cell> 1/18/2016 </Cell> </Row> </TableBody> </Table> | Name | Type | Date Modified | | --- | --- | --- | | Games | File folder | 6/7/2020 | | Program Files | File folder | 4/7/2021 | | bootmgr | System file | 11/20/2010 | | log.txt | Text Document | 1/18/2016 | ### Adding selection# Next, let's add support for selection. For multiple selection, we'll want to add a column of checkboxes to the left of the table to allow the user to select rows. This is done using the `useTableSelectionCheckbox` hook. It is passed the `parentKey` of the cell, which refers to the row the cell is contained within. When the user checks or unchecks the checkbox, the row will be added or removed from the Table's selection. The `Checkbox` component used in this example is independent and can be used separately from `Table`. The code is available below. See useCheckbox for documentation. import {useTableSelectionCheckbox} from 'react-aria'; // Reuse the Checkbox from your component library. See below for details. import {Checkbox} from 'your-component-library'; function TableCheckboxCell({ cell, state }) { let ref = useRef<HTMLTableCellElement | null>(null); let { gridCellProps } = useTableCell({ node: cell }, state, ref); let { checkboxProps } = useTableSelectionCheckbox( { key: cell.parentKey }, state ); return ( <td {...gridCellProps} ref={ref} > <Checkbox {...checkboxProps} /> </td> ); } import {useTableSelectionCheckbox} from 'react-aria'; // Reuse the Checkbox from your component library. See below for details. import {Checkbox} from 'your-component-library'; function TableCheckboxCell({ cell, state }) { let ref = useRef<HTMLTableCellElement | null>(null); let { gridCellProps } = useTableCell( { node: cell }, state, ref ); let { checkboxProps } = useTableSelectionCheckbox({ key: cell.parentKey }, state); return ( <td {...gridCellProps} ref={ref} > <Checkbox {...checkboxProps} /> </td> ); } import {useTableSelectionCheckbox} from 'react-aria'; // Reuse the Checkbox from your component library. See below for details. import {Checkbox} from 'your-component-library'; function TableCheckboxCell( { cell, state } ) { let ref = useRef< | HTMLTableCellElement | null >(null); let { gridCellProps } = useTableCell( { node: cell }, state, ref ); let { checkboxProps } = useTableSelectionCheckbox( { key: cell.parentKey }, state ); return ( <td {...gridCellProps} ref={ref} > <Checkbox {...checkboxProps} /> </td> ); } We also want the user to be able to select all rows in the table at once. This is possible using the ⌘ Cmd + A keyboard shortcut, but we'll also add a checkbox into the table header to do this and represent the selection state visually. This is done using the `useTableSelectAllCheckbox` hook. When all rows are selected, the checkbox will be shown as checked, and when only some rows are selected, the checkbox will be rendered in an indeterminate state. The user can check or uncheck the checkbox to select all or clear the selection, respectively. **Note**: Always ensure that the cell has accessible content, even when the checkbox is hidden (i.e. in single selection mode). The VisuallyHidden component can be used to do this. import {useTableSelectAllCheckbox, VisuallyHidden} from 'react-aria'; function TableSelectAllCell({ column, state }) { let ref = useRef<HTMLTableCellElement | null>(null); let { columnHeaderProps } = useTableColumnHeader( { node: column }, state, ref ); let { checkboxProps } = useTableSelectAllCheckbox(state); return ( <th {...columnHeaderProps} ref={ref} > {state.selectionManager.selectionMode === 'single' ? <VisuallyHidden>{checkboxProps['aria-label']}</VisuallyHidden> : <Checkbox {...checkboxProps} />} </th> ); } import { useTableSelectAllCheckbox, VisuallyHidden } from 'react-aria'; function TableSelectAllCell({ column, state }) { let ref = useRef<HTMLTableCellElement | null>(null); let { columnHeaderProps } = useTableColumnHeader( { node: column }, state, ref ); let { checkboxProps } = useTableSelectAllCheckbox(state); return ( <th {...columnHeaderProps} ref={ref} > {state.selectionManager.selectionMode === 'single' ? ( <VisuallyHidden> {checkboxProps['aria-label']} </VisuallyHidden> ) : <Checkbox {...checkboxProps} />} </th> ); } import { useTableSelectAllCheckbox, VisuallyHidden } from 'react-aria'; function TableSelectAllCell( { column, state } ) { let ref = useRef< | HTMLTableCellElement | null >(null); let { columnHeaderProps } = useTableColumnHeader( { node: column }, state, ref ); let { checkboxProps } = useTableSelectAllCheckbox( state ); return ( <th {...columnHeaderProps} ref={ref} > {state .selectionManager .selectionMode === 'single' ? ( <VisuallyHidden> {checkboxProps[ 'aria-label' ]} </VisuallyHidden> ) : ( <Checkbox {...checkboxProps} /> )} </th> ); } The following example shows how to enable multiple selection support using the Table component we built above. It's as simple as setting the `selectionMode` prop to `"multiple"`. Because we set the `showSelectionCheckboxes` option of `useTableState` to true when multiple selection is enabled, an extra column for these checkboxes is automatically added for us. And that's it! We now have a fully interactive table component that can support keyboard navigation, single or multiple selection, as well as column sorting. In addition, it is fully accessible for screen readers and other assistive technology. See below for more examples of how to use the Table component that we've built. <Table aria-label="Table with selection" selectionMode="multiple"> <TableHeader> <Column>Name</Column> <Column>Type</Column> <Column>Level</Column> </TableHeader> <TableBody> <Row key="1"> <Cell>Charizard</Cell> <Cell>Fire, Flying</Cell> <Cell>67</Cell> </Row> <Row key="2"> <Cell>Blastoise</Cell> <Cell>Water</Cell> <Cell>56</Cell> </Row> <Row key="3"> <Cell>Venusaur</Cell> <Cell>Grass, Poison</Cell> <Cell>83</Cell> </Row> <Row key="4"> <Cell>Pikachu</Cell> <Cell>Electric</Cell> <Cell>100</Cell> </Row> </TableBody> </Table> <Table aria-label="Table with selection" selectionMode="multiple" > <TableHeader> <Column>Name</Column> <Column>Type</Column> <Column>Level</Column> </TableHeader> <TableBody> <Row key="1"> <Cell>Charizard</Cell> <Cell>Fire, Flying</Cell> <Cell>67</Cell> </Row> <Row key="2"> <Cell>Blastoise</Cell> <Cell>Water</Cell> <Cell>56</Cell> </Row> <Row key="3"> <Cell>Venusaur</Cell> <Cell>Grass, Poison</Cell> <Cell>83</Cell> </Row> <Row key="4"> <Cell>Pikachu</Cell> <Cell>Electric</Cell> <Cell>100</Cell> </Row> </TableBody> </Table> <Table aria-label="Table with selection" selectionMode="multiple" > <TableHeader> <Column> Name </Column> <Column> Type </Column> <Column> Level </Column> </TableHeader> <TableBody> <Row key="1"> <Cell> Charizard </Cell> <Cell> Fire, Flying </Cell> <Cell>67</Cell> </Row> <Row key="2"> <Cell> Blastoise </Cell> <Cell> Water </Cell> <Cell>56</Cell> </Row> <Row key="3"> <Cell> Venusaur </Cell> <Cell> Grass, Poison </Cell> <Cell>83</Cell> </Row> <Row key="4"> <Cell> Pikachu </Cell> <Cell> Electric </Cell> <Cell>100</Cell> </Row> </TableBody> </Table> | | Name | Type | Level | | --- | --- | --- | --- | | | Charizard | Fire, Flying | 67 | | | Blastoise | Water | 56 | | | Venusaur | Grass, Poison | 83 | | | Pikachu | Electric | 100 | ### Checkbox# The `Checkbox` component used in the above example is used to implement row selection. It is built using the useCheckbox hook, and can be shared with many other components. Show code import {useCheckbox} from 'react-aria'; import {useToggleState} from 'react-stately'; function Checkbox(props) { let ref = React.useRef<HTMLInputElement | null>(null); let state = useToggleState(props); let { inputProps } = useCheckbox(props, state, ref); return <input {...inputProps} ref={ref} style={props.style} />; } import {useCheckbox} from 'react-aria'; import {useToggleState} from 'react-stately'; function Checkbox(props) { let ref = React.useRef<HTMLInputElement | null>(null); let state = useToggleState(props); let { inputProps } = useCheckbox(props, state, ref); return ( <input {...inputProps} ref={ref} style={props.style} /> ); } import {useCheckbox} from 'react-aria'; import {useToggleState} from 'react-stately'; function Checkbox( props ) { let ref = React.useRef< | HTMLInputElement | null >(null); let state = useToggleState( props ); let { inputProps } = useCheckbox( props, state, ref ); return ( <input {...inputProps} ref={ref} style={props.style} /> ); } ## Usage# * * * ### Dynamic collections# So far, our examples have shown static collections, where the data is hard coded. Dynamic collections, as shown below, can be used when the table data comes from an external data source such as an API, or updates over time. In the example below, both the columns and the rows are provided to the table via a render function. You can also make the columns static and only the rows dynamic. function ExampleTable(props) { let columns = [ {name: 'Name', key: 'name'}, {name: 'Type', key: 'type'}, {name: 'Date Modified', key: 'date'} ]; let rows = [ {id: 1, name: 'Games', date: '6/7/2020', type: 'File folder'}, {id: 2, name: 'Program Files', date: '4/7/2021', type: 'File folder'}, {id: 3, name: 'bootmgr', date: '11/20/2010', type: 'System file'}, {id: 4, name: 'log.txt', date: '1/18/2016', type: 'Text Document'} ]; return ( <Table aria-label="Example dynamic collection table" {...props}> <TableHeader columns={columns}> {column => ( <Column> {column.name} </Column> )} </TableHeader> <TableBody items={rows}> {item => ( <Row> {columnKey => <Cell>{item[columnKey]}</Cell>} </Row> )} </TableBody> </Table> ); } function ExampleTable(props) { let columns = [ { name: 'Name', key: 'name' }, { name: 'Type', key: 'type' }, { name: 'Date Modified', key: 'date' } ]; let rows = [ { id: 1, name: 'Games', date: '6/7/2020', type: 'File folder' }, { id: 2, name: 'Program Files', date: '4/7/2021', type: 'File folder' }, { id: 3, name: 'bootmgr', date: '11/20/2010', type: 'System file' }, { id: 4, name: 'log.txt', date: '1/18/2016', type: 'Text Document' } ]; return ( <Table aria-label="Example dynamic collection table" {...props} > <TableHeader columns={columns}> {(column) => ( <Column> {column.name} </Column> )} </TableHeader> <TableBody items={rows}> {(item) => ( <Row> {(columnKey) => <Cell>{item[columnKey]}</Cell>} </Row> )} </TableBody> </Table> ); } function ExampleTable( props ) { let columns = [ { name: 'Name', key: 'name' }, { name: 'Type', key: 'type' }, { name: 'Date Modified', key: 'date' } ]; let rows = [ { id: 1, name: 'Games', date: '6/7/2020', type: 'File folder' }, { id: 2, name: 'Program Files', date: '4/7/2021', type: 'File folder' }, { id: 3, name: 'bootmgr', date: '11/20/2010', type: 'System file' }, { id: 4, name: 'log.txt', date: '1/18/2016', type: 'Text Document' } ]; return ( <Table aria-label="Example dynamic collection table" {...props} > <TableHeader columns={columns} > {(column) => ( <Column> {column.name} </Column> )} </TableHeader> <TableBody items={rows} > {(item) => ( <Row> {(columnKey) => ( <Cell> {item[ columnKey ]} </Cell> )} </Row> )} </TableBody> </Table> ); } | Name | Type | Date Modified | | --- | --- | --- | | Games | File folder | 6/7/2020 | | Program Files | File folder | 4/7/2021 | | bootmgr | System file | 11/20/2010 | | log.txt | Text Document | 1/18/2016 | ### Single selection# By default, `useTableState` doesn't allow row selection but this can be enabled using the `selectionMode` prop. Use `defaultSelectedKeys` to provide a default set of selected rows. Note that the value of the selected keys must match the `key` prop of the row. The example below enables single selection mode, and uses `defaultSelectedKeys` to select the row with key equal to "2". A user can click on a different row to change the selection, or click on the same row again to deselect it entirely. // Using the example above <ExampleTable selectionMode="single" defaultSelectedKeys={[2]} /> // Using the example above <ExampleTable selectionMode="single" defaultSelectedKeys={[2]} /> // Using the example above <ExampleTable selectionMode="single" defaultSelectedKeys={[ 2 ]} /> | Name | Type | Date Modified | | --- | --- | --- | | Games | File folder | 6/7/2020 | | Program Files | File folder | 4/7/2021 | | bootmgr | System file | 11/20/2010 | | log.txt | Text Document | 1/18/2016 | ### Multiple selection# Multiple selection can be enabled by setting `selectionMode` to `multiple`. // Using the example above <ExampleTable selectionMode="multiple" defaultSelectedKeys={[2, 4]} /> // Using the example above <ExampleTable selectionMode="multiple" defaultSelectedKeys={[2, 4]} /> // Using the example above <ExampleTable selectionMode="multiple" defaultSelectedKeys={[ 2, 4 ]} /> | | Name | Type | Date Modified | | --- | --- | --- | --- | | | Games | File folder | 6/7/2020 | | | Program Files | File folder | 4/7/2021 | | | bootmgr | System file | 11/20/2010 | | | log.txt | Text Document | 1/18/2016 | ### Disallow empty selection# Table also supports a `disallowEmptySelection` prop which forces the user to have at least one row in the Table selected at all times. In this mode, if a single row is selected and the user presses it, it will not be deselected. // Using the example above <ExampleTable selectionMode="single" defaultSelectedKeys={[2]} disallowEmptySelection /> // Using the example above <ExampleTable selectionMode="single" defaultSelectedKeys={[2]} disallowEmptySelection /> // Using the example above <ExampleTable selectionMode="single" defaultSelectedKeys={[ 2 ]} disallowEmptySelection /> | Name | Type | Date Modified | | --- | --- | --- | | Games | File folder | 6/7/2020 | | Program Files | File folder | 4/7/2021 | | bootmgr | System file | 11/20/2010 | | log.txt | Text Document | 1/18/2016 | ### Controlled selection# To programmatically control row selection, use the `selectedKeys` prop paired with the `onSelectionChange` callback. The `key` prop from the selected rows will be passed into the callback when the row is pressed, allowing you to update state accordingly. import type {Selection} from 'react-stately'; function PokemonTable(props) { let columns = [ { name: 'Name', uid: 'name' }, { name: 'Type', uid: 'type' }, { name: 'Level', uid: 'level' } ]; let rows = [ { id: 1, name: 'Charizard', type: 'Fire, Flying', level: '67' }, { id: 2, name: 'Blastoise', type: 'Water', level: '56' }, { id: 3, name: 'Venusaur', type: 'Grass, Poison', level: '83' }, { id: 4, name: 'Pikachu', type: 'Electric', level: '100' } ]; let [selectedKeys, setSelectedKeys] = React.useState<Selection>(new Set([2])); return ( <Table aria-label="Table with controlled selection" selectionMode="multiple" selectedKeys={selectedKeys} onSelectionChange={setSelectedKeys} {...props} > <TableHeader columns={columns}> {(column) => ( <Column key={column.uid}> {column.name} </Column> )} </TableHeader> <TableBody items={rows}> {(item) => ( <Row> {(columnKey) => <Cell>{item[columnKey]}</Cell>} </Row> )} </TableBody> </Table> ); } import type {Selection} from 'react-stately'; function PokemonTable(props) { let columns = [ { name: 'Name', uid: 'name' }, { name: 'Type', uid: 'type' }, { name: 'Level', uid: 'level' } ]; let rows = [ { id: 1, name: 'Charizard', type: 'Fire, Flying', level: '67' }, { id: 2, name: 'Blastoise', type: 'Water', level: '56' }, { id: 3, name: 'Venusaur', type: 'Grass, Poison', level: '83' }, { id: 4, name: 'Pikachu', type: 'Electric', level: '100' } ]; let [selectedKeys, setSelectedKeys] = React.useState< Selection >(new Set([2])); return ( <Table aria-label="Table with controlled selection" selectionMode="multiple" selectedKeys={selectedKeys} onSelectionChange={setSelectedKeys} {...props} > <TableHeader columns={columns}> {(column) => ( <Column key={column.uid}> {column.name} </Column> )} </TableHeader> <TableBody items={rows}> {(item) => ( <Row> {(columnKey) => <Cell>{item[columnKey]}</Cell>} </Row> )} </TableBody> </Table> ); } import type {Selection} from 'react-stately'; function PokemonTable( props ) { let columns = [ { name: 'Name', uid: 'name' }, { name: 'Type', uid: 'type' }, { name: 'Level', uid: 'level' } ]; let rows = [ { id: 1, name: 'Charizard', type: 'Fire, Flying', level: '67' }, { id: 2, name: 'Blastoise', type: 'Water', level: '56' }, { id: 3, name: 'Venusaur', type: 'Grass, Poison', level: '83' }, { id: 4, name: 'Pikachu', type: 'Electric', level: '100' } ]; let [ selectedKeys, setSelectedKeys ] = React.useState< Selection >(new Set([2])); return ( <Table aria-label="Table with controlled selection" selectionMode="multiple" selectedKeys={selectedKeys} onSelectionChange={setSelectedKeys} {...props} > <TableHeader columns={columns} > {(column) => ( <Column key={column .uid} > {column.name} </Column> )} </TableHeader> <TableBody items={rows} > {(item) => ( <Row> {(columnKey) => ( <Cell> {item[ columnKey ]} </Cell> )} </Row> )} </TableBody> </Table> ); } | | Name | Type | Level | | --- | --- | --- | --- | | | Charizard | Fire, Flying | 67 | | | Blastoise | Water | 56 | | | Venusaur | Grass, Poison | 83 | | | Pikachu | Electric | 100 | ### Disabled rows# You can disable specific rows by providing an array of keys to `useTableState` via the `disabledKeys` prop. This will prevent rows from being selectable as shown in the example below. Note that you are responsible for the styling of disabled rows, however, the selection checkbox will be automatically disabled. // Using the same table as above <PokemonTable selectionMode="multiple" disabledKeys={[3]} /> // Using the same table as above <PokemonTable selectionMode="multiple" disabledKeys={[3]} /> // Using the same table as above <PokemonTable selectionMode="multiple" disabledKeys={[3]} /> | | Name | Type | Level | | --- | --- | --- | --- | | | Charizard | Fire, Flying | 67 | | | Blastoise | Water | 56 | | | Venusaur | Grass, Poison | 83 | | | Pikachu | Electric | 100 | ### Selection behavior# By default, `useTable` uses the `"toggle"` selection behavior, which behaves like a checkbox group: clicking, tapping, or pressing the Space or Enter keys toggles selection for the focused row. Using the arrow keys moves focus but does not change selection. The `"toggle"` selection mode is often paired with a column of checkboxes in each row as an explicit affordance for selection. When the `selectionBehavior` prop is set to `"replace"`, clicking a row with the mouse _replaces_ the selection with only that row. Using the arrow keys moves both focus and selection. To select multiple rows, modifier keys such as Ctrl, Cmd, and Shift can be used. To move focus without moving selection, the Ctrl key on Windows or the Option key on macOS can be held while pressing the arrow keys. Holding this modifier while pressing the Space key toggles selection for the focused row, which allows multiple selection of non-contiguous items. On touch screen devices, selection always behaves as toggle since modifier keys may not be available. This behavior emulates native platforms such as macOS and Windows, and is often used when checkboxes in each row are not desired. <PokemonTable selectionMode="multiple" selectionBehavior="replace" /> <PokemonTable selectionMode="multiple" selectionBehavior="replace" /> <PokemonTable selectionMode="multiple" selectionBehavior="replace" /> | Name | Type | Level | | --- | --- | --- | | Charizard | Fire, Flying | 67 | | Blastoise | Water | 56 | | Venusaur | Grass, Poison | 83 | | Pikachu | Electric | 100 | ### Row actions# `useTable` supports row actions via the `onRowAction` prop, which is useful for functionality such as navigation. In the default `"toggle"` selection behavior, when nothing is selected, clicking or tapping the row triggers the row action. When at least one item is selected, the table is in selection mode, and clicking or tapping a row toggles the selection. Actions may also be triggered via the Enter key, and selection using the Space key. This behavior is slightly different in the `"replace"` selection behavior, where single clicking selects the row and actions are performed via double click. On touch devices, the action becomes the primary tap interaction, and a long press enters into selection mode, which temporarily swaps the selection behavior to `"toggle"` to perform selection (you may wish to display checkboxes when this happens). Deselecting all items exits selection mode and reverts the selection behavior back to `"replace"`. Keyboard behaviors are unaffected. <div style={{ display: 'flex', flexWrap: 'wrap', gap: '24px' }}> <PokemonTable aria-label="Pokemon table with row actions and toggle selection behavior" selectionMode="multiple" onRowAction={(key) => alert(`Opening item ${key}...`)} /> <PokemonTable aria-label="Pokemon table with row actions and replace selection behavior" selectionMode="multiple" selectionBehavior="replace" onRowAction={(key) => alert(`Opening item ${key}...`)} /> </div> <div style={{ display: 'flex', flexWrap: 'wrap', gap: '24px' }} > <PokemonTable aria-label="Pokemon table with row actions and toggle selection behavior" selectionMode="multiple" onRowAction={(key) => alert(`Opening item ${key}...`)} /> <PokemonTable aria-label="Pokemon table with row actions and replace selection behavior" selectionMode="multiple" selectionBehavior="replace" onRowAction={(key) => alert(`Opening item ${key}...`)} /> </div> <div style={{ display: 'flex', flexWrap: 'wrap', gap: '24px' }} > <PokemonTable aria-label="Pokemon table with row actions and toggle selection behavior" selectionMode="multiple" onRowAction={(key) => alert( `Opening item ${key}...` )} /> <PokemonTable aria-label="Pokemon table with row actions and replace selection behavior" selectionMode="multiple" selectionBehavior="replace" onRowAction={(key) => alert( `Opening item ${key}...` )} /> </div> | | Name | Type | Level | | --- | --- | --- | --- | | | Charizard | Fire, Flying | 67 | | | Blastoise | Water | 56 | | | Venusaur | Grass, Poison | 83 | | | Pikachu | Electric | 100 | | Name | Type | Level | | --- | --- | --- | | Charizard | Fire, Flying | 67 | | Blastoise | Water | 56 | | Venusaur | Grass, Poison | 83 | | Pikachu | Electric | 100 | ### Links# Table rows may also be links to another page or website. This can be achieved by passing the `href` prop to the `<Row>` component. Links behave the same way as described above for row actions depending on the `selectionMode` and `selectionBehavior`. <Table aria-label="Bookmarks" selectionMode="multiple"> <TableHeader> <Column isRowHeader>Name</Column> <Column>URL</Column> <Column>Date added</Column> </TableHeader> <TableBody> <Row href="https://adobe.com/" target="_blank"> <Cell>Adobe</Cell> <Cell>https://adobe.com/</Cell> <Cell>January 28, 2023</Cell> </Row> <Row href="https://google.com/" target="_blank"> <Cell>Google</Cell> <Cell>https://google.com/</Cell> <Cell>April 5, 2023</Cell> </Row> <Row href="https://nytimes.com/" target="_blank"> <Cell>New York Times</Cell> <Cell>https://nytimes.com/</Cell> <Cell>July 12, 2023</Cell> </Row> </TableBody> </Table> <Table aria-label="Bookmarks" selectionMode="multiple"> <TableHeader> <Column isRowHeader>Name</Column> <Column>URL</Column> <Column>Date added</Column> </TableHeader> <TableBody> <Row href="https://adobe.com/" target="_blank"> <Cell>Adobe</Cell> <Cell>https://adobe.com/</Cell> <Cell>January 28, 2023</Cell> </Row> <Row href="https://google.com/" target="_blank"> <Cell>Google</Cell> <Cell>https://google.com/</Cell> <Cell>April 5, 2023</Cell> </Row> <Row href="https://nytimes.com/" target="_blank"> <Cell>New York Times</Cell> <Cell>https://nytimes.com/</Cell> <Cell>July 12, 2023</Cell> </Row> </TableBody> </Table> <Table aria-label="Bookmarks" selectionMode="multiple" > <TableHeader> <Column isRowHeader > Name </Column> <Column> URL </Column> <Column> Date added </Column> </TableHeader> <TableBody> <Row href="https://adobe.com/" target="_blank" > <Cell> Adobe </Cell> <Cell> https://adobe.com/ </Cell> <Cell> January 28, 2023 </Cell> </Row> <Row href="https://google.com/" target="_blank" > <Cell> Google </Cell> <Cell> https://google.com/ </Cell> <Cell> April 5, 2023 </Cell> </Row> <Row href="https://nytimes.com/" target="_blank" > <Cell> New York Times </Cell> <Cell> https://nytimes.com/ </Cell> <Cell> July 12, 2023 </Cell> </Row> </TableBody> </Table> | | Name | URL | Date added | | --- | --- | --- | --- | | | Adobe | https://adobe.com/ | January 28, 2023 | | | Google | https://google.com/ | April 5, 2023 | | | New York Times | https://nytimes.com/ | July 12, 2023 | #### Client side routing# The `<Row>` component works with frameworks and client side routers like Next.js and React Router. As with other React Aria components that support links, this works via the `RouterProvider` component at the root of your app. See the client side routing guide to learn how to set this up. ### Sorting# Table supports sorting its data when a column header is pressed. To designate that a Column should support sorting, provide it with the `allowsSorting` prop. The Table accepts a `sortDescriptor` prop that defines the current column key to sort by and the sort direction (ascending/descending). When the user presses a sortable column header, the column's key and sort direction is passed into the `onSortChange` callback, allowing you to update the `sortDescriptor` appropriately. This example performs client side sorting by passing a `sort` function to the useAsyncList hook. See the docs for more information on how to perform server side sorting. import {useAsyncList} from 'react-stately'; interface Character { name: string; height: number; mass: number; birth_year: number; } function AsyncSortTable() { let list = useAsyncList<Character>({ async load({ signal }) { let res = await fetch(`https://swapi.py4e.com/api/people/?search`, { signal }); let json = await res.json(); return { items: json.results }; }, async sort({ items, sortDescriptor }) { return { items: items.sort((a, b) => { let first = a[sortDescriptor.column]; let second = b[sortDescriptor.column]; let cmp = (parseInt(first) || first) < (parseInt(second) || second) ? -1 : 1; if (sortDescriptor.direction === 'descending') { cmp *= -1; } return cmp; }) }; } }); return ( <Table aria-label="Example table with client side sorting" sortDescriptor={list.sortDescriptor} onSortChange={list.sort} > <TableHeader> <Column key="name" allowsSorting>Name</Column> <Column key="height" allowsSorting>Height</Column> <Column key="mass" allowsSorting>Mass</Column> <Column key="birth_year" allowsSorting>Birth Year</Column> </TableHeader> <TableBody items={list.items}> {(item) => ( <Row key={item.name}> {(columnKey) => <Cell>{item[columnKey]}</Cell>} </Row> )} </TableBody> </Table> ); } import {useAsyncList} from 'react-stately'; interface Character { name: string; height: number; mass: number; birth_year: number; } function AsyncSortTable() { let list = useAsyncList<Character>({ async load({ signal }) { let res = await fetch( `https://swapi.py4e.com/api/people/?search`, { signal } ); let json = await res.json(); return { items: json.results }; }, async sort({ items, sortDescriptor }) { return { items: items.sort((a, b) => { let first = a[sortDescriptor.column]; let second = b[sortDescriptor.column]; let cmp = (parseInt(first) || first) < (parseInt(second) || second) ? -1 : 1; if (sortDescriptor.direction === 'descending') { cmp *= -1; } return cmp; }) }; } }); return ( <Table aria-label="Example table with client side sorting" sortDescriptor={list.sortDescriptor} onSortChange={list.sort} > <TableHeader> <Column key="name" allowsSorting>Name</Column> <Column key="height" allowsSorting>Height</Column> <Column key="mass" allowsSorting>Mass</Column> <Column key="birth_year" allowsSorting> Birth Year </Column> </TableHeader> <TableBody items={list.items}> {(item) => ( <Row key={item.name}> {(columnKey) => <Cell>{item[columnKey]}</Cell>} </Row> )} </TableBody> </Table> ); } import {useAsyncList} from 'react-stately'; interface Character { name: string; height: number; mass: number; birth_year: number; } function AsyncSortTable() { let list = useAsyncList< Character >({ async load( { signal } ) { let res = await fetch( `https://swapi.py4e.com/api/people/?search`, { signal } ); let json = await res .json(); return { items: json.results }; }, async sort( { items, sortDescriptor } ) { return { items: items .sort( (a, b) => { let first = a[ sortDescriptor .column ]; let second = b[ sortDescriptor .column ]; let cmp = (parseInt( first ) || first) < (parseInt( second ) || second) ? -1 : 1; if ( sortDescriptor .direction === 'descending' ) { cmp *= -1; } return cmp; } ) }; } }); return ( <Table aria-label="Example table with client side sorting" sortDescriptor={list .sortDescriptor} onSortChange={list .sort} > <TableHeader> <Column key="name" allowsSorting > Name </Column> <Column key="height" allowsSorting > Height </Column> <Column key="mass" allowsSorting > Mass </Column> <Column key="birth_year" allowsSorting > Birth Year </Column> </TableHeader> <TableBody items={list .items} > {(item) => ( <Row key={item .name} > {(columnKey) => ( <Cell> {item[ columnKey ]} </Cell> )} </Row> )} </TableBody> </Table> ); } | Name▼ | Height▼ | Mass▼ | Birth Year▼ | | --- | --- | --- | --- | | Luke Skywalker | 172 | 77 | 19BBY | | C-3PO | 167 | 75 | 112BBY | | R2-D2 | 96 | 32 | 33BBY | | Darth Vader | 202 | 136 | 41.9BBY | | Leia Organa | 150 | 49 | 19BBY | | Owen Lars | 178 | 120 | 52BBY | | Beru Whitesun lars | 165 | 75 | 47BBY | | R5-D4 | 97 | 32 | unknown | | Biggs Darklighter | 183 | 84 | 24BBY | | Obi-Wan Kenobi | 182 | 77 | 57BBY | ### Nested columns# Columns can be nested to create column groups. This will result in more than one header row to be created, with the `colSpan` attribute of each column header cell set to the appropriate value so that the columns line up. Data for the leaf columns appears in each row of the table body. This example also shows the use of the `isRowHeader` prop for `Column`, which controls which columns are included in the accessibility name for each row. By default, only the first column is included, but in some cases more than one column may be used to represent the row. In this example, the first and last name columns are combined to form the ARIA label for the row. Only leaf columns may be marked as row headers. <Table aria-label="Example table with nested columns"> <TableHeader> <Column title="Name"> <Column isRowHeader>First Name</Column> <Column isRowHeader>Last Name</Column> </Column> <Column title="Information"> <Column>Age</Column> <Column>Birthday</Column> </Column> </TableHeader> <TableBody> <Row> <Cell>Sam</Cell> <Cell>Smith</Cell> <Cell>36</Cell> <Cell>May 3</Cell> </Row> <Row> <Cell>Julia</Cell> <Cell>Jones</Cell> <Cell>24</Cell> <Cell>February 10</Cell> </Row> <Row> <Cell>Peter</Cell> <Cell>Parker</Cell> <Cell>28</Cell> <Cell>September 7</Cell> </Row> <Row> <Cell>Bruce</Cell> <Cell>Wayne</Cell> <Cell>32</Cell> <Cell>December 18</Cell> </Row> </TableBody> </Table> <Table aria-label="Example table with nested columns"> <TableHeader> <Column title="Name"> <Column isRowHeader>First Name</Column> <Column isRowHeader>Last Name</Column> </Column> <Column title="Information"> <Column>Age</Column> <Column>Birthday</Column> </Column> </TableHeader> <TableBody> <Row> <Cell>Sam</Cell> <Cell>Smith</Cell> <Cell>36</Cell> <Cell>May 3</Cell> </Row> <Row> <Cell>Julia</Cell> <Cell>Jones</Cell> <Cell>24</Cell> <Cell>February 10</Cell> </Row> <Row> <Cell>Peter</Cell> <Cell>Parker</Cell> <Cell>28</Cell> <Cell>September 7</Cell> </Row> <Row> <Cell>Bruce</Cell> <Cell>Wayne</Cell> <Cell>32</Cell> <Cell>December 18</Cell> </Row> </TableBody> </Table> <Table aria-label="Example table with nested columns"> <TableHeader> <Column title="Name"> <Column isRowHeader > First Name </Column> <Column isRowHeader > Last Name </Column> </Column> <Column title="Information"> <Column> Age </Column> <Column> Birthday </Column> </Column> </TableHeader> <TableBody> <Row> <Cell>Sam</Cell> <Cell> Smith </Cell> <Cell>36</Cell> <Cell> May 3 </Cell> </Row> <Row> <Cell> Julia </Cell> <Cell> Jones </Cell> <Cell>24</Cell> <Cell> February 10 </Cell> </Row> <Row> <Cell> Peter </Cell> <Cell> Parker </Cell> <Cell>28</Cell> <Cell> September 7 </Cell> </Row> <Row> <Cell> Bruce </Cell> <Cell> Wayne </Cell> <Cell>32</Cell> <Cell> December 18 </Cell> </Row> </TableBody> </Table> | Name | Information | | --- | --- | | First Name | Last Name | Age | Birthday | | --- | --- | --- | --- | | Sam | Smith | 36 | May 3 | | Julia | Jones | 24 | February 10 | | Peter | Parker | 28 | September 7 | | Bruce | Wayne | 32 | December 18 | ### Dynamic nested columns# Nested columns can also be defined dynamically using the function syntax and the `childColumns` prop. The following example is the same as the example above, but defined dynamically. interface ColumnDefinition { name: string, key: string, children?: ColumnDefinition[], isRowHeader?: boolean } let columns: ColumnDefinition[] = [ {name: 'Name', key: 'name', children: [ {name: 'First Name', key: 'first', isRowHeader: true}, {name: 'Last Name', key: 'last', isRowHeader: true} ]}, {name: 'Information', key: 'info', children: [ {name: 'Age', key: 'age'}, {name: 'Birthday', key: 'birthday'} ]} ]; let rows = [ {id: 1, first: 'Sam', last: 'Smith', age: 36, birthday: 'May 3'}, {id: 2, first: 'Julia', last: 'Jones', age: 24, birthday: 'February 10'}, {id: 3, first: 'Peter', last: 'Parker', age: 28, birthday: 'September 7'}, {id: 4, first: 'Bruce', last: 'Wayne', age: 32, birthday: 'December 18'} ]; <Table aria-label="Example table with dynamic nested columns"> <TableHeader columns={columns}> {column => ( <Column isRowHeader={column.isRowHeader} childColumns={column.children}> {column.name} </Column> )} </TableHeader> <TableBody items={rows}> {item => ( <Row> {columnKey => <Cell>{item[columnKey]}</Cell>} </Row> )} </TableBody> </Table> interface ColumnDefinition { name: string; key: string; children?: ColumnDefinition[]; isRowHeader?: boolean; } let columns: ColumnDefinition[] = [ { name: 'Name', key: 'name', children: [ { name: 'First Name', key: 'first', isRowHeader: true }, { name: 'Last Name', key: 'last', isRowHeader: true } ] }, { name: 'Information', key: 'info', children: [ { name: 'Age', key: 'age' }, { name: 'Birthday', key: 'birthday' } ] } ]; let rows = [ { id: 1, first: 'Sam', last: 'Smith', age: 36, birthday: 'May 3' }, { id: 2, first: 'Julia', last: 'Jones', age: 24, birthday: 'February 10' }, { id: 3, first: 'Peter', last: 'Parker', age: 28, birthday: 'September 7' }, { id: 4, first: 'Bruce', last: 'Wayne', age: 32, birthday: 'December 18' } ]; <Table aria-label="Example table with dynamic nested columns"> <TableHeader columns={columns}> {(column) => ( <Column isRowHeader={column.isRowHeader} childColumns={column.children} > {column.name} </Column> )} </TableHeader> <TableBody items={rows}> {(item) => ( <Row> {(columnKey) => <Cell>{item[columnKey]}</Cell>} </Row> )} </TableBody> </Table> interface ColumnDefinition { name: string; key: string; children?: ColumnDefinition[]; isRowHeader?: boolean; } let columns: ColumnDefinition[] = [ { name: 'Name', key: 'name', children: [ { name: 'First Name', key: 'first', isRowHeader: true }, { name: 'Last Name', key: 'last', isRowHeader: true } ] }, { name: 'Information', key: 'info', children: [ { name: 'Age', key: 'age' }, { name: 'Birthday', key: 'birthday' } ] } ]; let rows = [ { id: 1, first: 'Sam', last: 'Smith', age: 36, birthday: 'May 3' }, { id: 2, first: 'Julia', last: 'Jones', age: 24, birthday: 'February 10' }, { id: 3, first: 'Peter', last: 'Parker', age: 28, birthday: 'September 7' }, { id: 4, first: 'Bruce', last: 'Wayne', age: 32, birthday: 'December 18' } ]; <Table aria-label="Example table with dynamic nested columns"> <TableHeader columns={columns} > {(column) => ( <Column isRowHeader={column .isRowHeader} childColumns={column .children} > {column.name} </Column> )} </TableHeader> <TableBody items={rows} > {(item) => ( <Row> {(columnKey) => ( <Cell> {item[ columnKey ]} </Cell> )} </Row> )} </TableBody> </Table> | Name | Information | | --- | --- | | First Name | Last Name | Age | Birthday | | --- | --- | --- | --- | | Sam | Smith | 36 | May 3 | | Julia | Jones | 24 | February 10 | | Peter | Parker | 28 | September 7 | | Bruce | Wayne | 32 | December 18 | ## Resizable Columns# * * * For resizable column support, two additional hooks need to be added to the table implementation above. The `useTableColumnResizeState` hook from `@react-stately/table` is responsible for initializing and tracking the widths of every column in your table, returning functions that you can use to update the column widths during a column resize operation. Note that this state is supplementary to the state returned by` useTableState `. The second column resizing hook is `useTableColumnResize` . This hook handles the interactions for a table column's resizer element, allowing the user to drag the resizer or use the keyboard arrows to expand the column's width. Be sure to pass the state returned by` useTableColumnResizeState `to this hook so the tracked widths can be updated appropriately. We'll walk through all the required changes to the previous table implementation step by step below. For simplicity's sake, we'll be omitting support for selection, sorting, and nested columns. ### Table# As mentioned previously, we first need to call `useTableColumnResizeState` to initialize the widths for our table's columns. We'll pass the state returned by` useTableColumnResizeState `along with any user defined `onResize` handlers to our `ResizableTableColumnHeaders` so it can be used by` useTableColumnResize `. The various style changes below are to add a wrapper div so the table is scrollable when the row content overflows and to support table body/column widths greater than the 300px applied to the table itself. import {useTableColumnResizeState} from 'react-stately'; import {useCallback} from 'react'; function ResizableColumnsTable(props) { let state = useTableState(props); let scrollRef = useRef<HTMLDivElement | null>(null); let ref = useRef<HTMLTableElement | null>(null); let { collection } = state; let { gridProps } = useTable( { ...props, // The table wrapper is scrollable rather than just the body scrollRef }, state, ref ); // Set the minimum width of the columns to 40px let getDefaultMinWidth = useCallback(() => { return 40; }, []); let layoutState = useTableColumnResizeState({ // Matches the width of the table itself tableWidth: 300, getDefaultMinWidth }, state); return ( <div className="aria-table-wrapper" ref={scrollRef}> <table {...gridProps} className="aria-table" ref={ref} > <TableRowGroup type="thead"> {collection.headerRows.map((headerRow) => ( <TableHeaderRow key={headerRow.key} item={headerRow} state={state}> {[...headerRow.childNodes].map((column) => ( <ResizableTableColumnHeader key={column.key} column={column} state={state} layoutState={layoutState} onResizeStart={props.onResizeStart} onResize={props.onResize} onResizeEnd={props.onResizeEnd} /> ))} </TableHeaderRow> ))} </TableRowGroup> <TableRowGroup type="tbody"> {[...collection.body.childNodes].map((row) => ( <TableRow key={row.key} item={row} state={state}> {[...row.childNodes].map((cell) => ( <TableCell key={cell.key} cell={cell} state={state} /> ))} </TableRow> ))} </TableRowGroup> </table> </div> ); } import {useTableColumnResizeState} from 'react-stately'; import {useCallback} from 'react'; function ResizableColumnsTable(props) { let state = useTableState(props); let scrollRef = useRef<HTMLDivElement | null>(null); let ref = useRef<HTMLTableElement | null>(null); let { collection } = state; let { gridProps } = useTable( { ...props, // The table wrapper is scrollable rather than just the body scrollRef }, state, ref ); // Set the minimum width of the columns to 40px let getDefaultMinWidth = useCallback(() => { return 40; }, []); let layoutState = useTableColumnResizeState({ // Matches the width of the table itself tableWidth: 300, getDefaultMinWidth }, state); return ( <div className="aria-table-wrapper" ref={scrollRef}> <table {...gridProps} className="aria-table" ref={ref} > <TableRowGroup type="thead"> {collection.headerRows.map((headerRow) => ( <TableHeaderRow key={headerRow.key} item={headerRow} state={state} > {[...headerRow.childNodes].map((column) => ( <ResizableTableColumnHeader key={column.key} column={column} state={state} layoutState={layoutState} onResizeStart={props.onResizeStart} onResize={props.onResize} onResizeEnd={props.onResizeEnd} /> ))} </TableHeaderRow> ))} </TableRowGroup> <TableRowGroup type="tbody"> {[...collection.body.childNodes].map((row) => ( <TableRow key={row.key} item={row} state={state} > {[...row.childNodes].map((cell) => ( <TableCell key={cell.key} cell={cell} state={state} /> ))} </TableRow> ))} </TableRowGroup> </table> </div> ); } import {useTableColumnResizeState} from 'react-stately'; import {useCallback} from 'react'; function ResizableColumnsTable( props ) { let state = useTableState(props); let scrollRef = useRef< HTMLDivElement | null >(null); let ref = useRef< | HTMLTableElement | null >(null); let { collection } = state; let { gridProps } = useTable( { ...props, // The table wrapper is scrollable rather than just the body scrollRef }, state, ref ); // Set the minimum width of the columns to 40px let getDefaultMinWidth = useCallback(() => { return 40; }, []); let layoutState = useTableColumnResizeState( { // Matches the width of the table itself tableWidth: 300, getDefaultMinWidth }, state ); return ( <div className="aria-table-wrapper" ref={scrollRef} > <table {...gridProps} className="aria-table" ref={ref} > <TableRowGroup type="thead"> {collection .headerRows .map( (headerRow) => ( <TableHeaderRow key={headerRow .key} item={headerRow} state={state} > {[ ...headerRow .childNodes ].map( (column) => ( <ResizableTableColumnHeader key={column .key} column={column} state={state} layoutState={layoutState} onResizeStart={props .onResizeStart} onResize={props .onResize} onResizeEnd={props .onResizeEnd} /> ) )} </TableHeaderRow> ) )} </TableRowGroup> <TableRowGroup type="tbody"> {[ ...collection .body .childNodes ].map( (row) => ( <TableRow key={row .key} item={row} state={state} > {[ ...row .childNodes ].map( (cell) => ( <TableCell key={cell .key} cell={cell} state={state} /> ) )} </TableRow> ) )} </TableRowGroup> </table> </div> ); } Show CSS .aria-table-wrapper { width: 300px; overflow: auto; } .aria-table { border-collapse: collapse; table-layout: fixed; width: fit-content; & td { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; } } .aria-table-wrapper { width: 300px; overflow: auto; } .aria-table { border-collapse: collapse; table-layout: fixed; width: fit-content; & td { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; } } .aria-table-wrapper { width: 300px; overflow: auto; } .aria-table { border-collapse: collapse; table-layout: fixed; width: fit-content; & td { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; } } ### Resizable table header# The `TableColumnHeader` is where we see the bulk of the changes required to support resizable columns. First of all, we need to accommodate a `Resizer` element in every resizable column that the user can drag or focus to perform a resize operation. Since the resizer will be a focusable element within the table header, we need to make the header title a focusable element as well so keyboard focus won't be immediately sent to the resizer as you navigate between the column headers. Finally, we apply the computed width of our column from `useTableColumnResizeState` to the header element. // Reuse the Button from your component library. See below for details. import {Button} from 'your-component-library'; function ResizableTableColumnHeader( { column, state, layoutState, onResizeStart, onResize, onResizeEnd } ) { let allowsResizing = column.props.allowsResizing; let ref = useRef<HTMLTableCellElement | null>(null); let { columnHeaderProps } = useTableColumnHeader( { node: column }, state, ref ); return ( <th {...columnHeaderProps} className="aria-table-headerCell" style={{ width: layoutState.getColumnWidth(column.key) }} ref={ref} > <div style={{ display: 'flex', position: 'relative' }}> <Button className="aria-table-headerTitle"> {column.rendered} </Button> {allowsResizing && ( <Resizer column={column} layoutState={layoutState} onResizeStart={onResizeStart} onResize={onResize} onResizeEnd={onResizeEnd} /> )} </div> </th> ); } // Reuse the Button from your component library. See below for details. import {Button} from 'your-component-library'; function ResizableTableColumnHeader( { column, state, layoutState, onResizeStart, onResize, onResizeEnd } ) { let allowsResizing = column.props.allowsResizing; let ref = useRef<HTMLTableCellElement | null>(null); let { columnHeaderProps } = useTableColumnHeader( { node: column }, state, ref ); return ( <th {...columnHeaderProps} className="aria-table-headerCell" style={{ width: layoutState.getColumnWidth(column.key) }} ref={ref} > <div style={{ display: 'flex', position: 'relative' }} > <Button className="aria-table-headerTitle"> {column.rendered} </Button> {allowsResizing && ( <Resizer column={column} layoutState={layoutState} onResizeStart={onResizeStart} onResize={onResize} onResizeEnd={onResizeEnd} /> )} </div> </th> ); } // Reuse the Button from your component library. See below for details. import {Button} from 'your-component-library'; function ResizableTableColumnHeader( { column, state, layoutState, onResizeStart, onResize, onResizeEnd } ) { let allowsResizing = column.props .allowsResizing; let ref = useRef< | HTMLTableCellElement | null >(null); let { columnHeaderProps } = useTableColumnHeader( { node: column }, state, ref ); return ( <th {...columnHeaderProps} className="aria-table-headerCell" style={{ width: layoutState .getColumnWidth( column.key ) }} ref={ref} > <div style={{ display: 'flex', position: 'relative' }} > <Button className="aria-table-headerTitle"> {column .rendered} </Button> {allowsResizing && ( <Resizer column={column} layoutState={layoutState} onResizeStart={onResizeStart} onResize={onResize} onResizeEnd={onResizeEnd} /> )} </div> </th> ); } Show CSS .aria-table-headerCell { padding: 5px 10px; outline: none; cursor: default; box-sizing: border-box; box-shadow: none; text-align: left; } .aria-table-headerTitle { width: 100%; text-align: left; border: none; background: transparent; flex: 1 1 auto; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; margin-inline-start: -6px; outline: none; } .aria-table-headerTitle.focus { outline: 2px solid orange; } .aria-table-headerCell { padding: 5px 10px; outline: none; cursor: default; box-sizing: border-box; box-shadow: none; text-align: left; } .aria-table-headerTitle { width: 100%; text-align: left; border: none; background: transparent; flex: 1 1 auto; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; margin-inline-start: -6px; outline: none; } .aria-table-headerTitle.focus { outline: 2px solid orange; } .aria-table-headerCell { padding: 5px 10px; outline: none; cursor: default; box-sizing: border-box; box-shadow: none; text-align: left; } .aria-table-headerTitle { width: 100%; text-align: left; border: none; background: transparent; flex: 1 1 auto; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; margin-inline-start: -6px; outline: none; } .aria-table-headerTitle.focus { outline: 2px solid orange; } ### Button# The `Button` component is used in the above example to represent the table column header title. It is built using the useButton hook, and can be shared with many other components. Show code import {useButton} from 'react-aria'; function Button(props) { let ref = useRef<HTMLButtonElement | null>(null); let { focusProps, isFocusVisible } = useFocusRing(); let { buttonProps } = useButton(props, ref); return ( <button {...mergeProps(buttonProps, focusProps)} ref={ref} className={`${props.className} ${isFocusVisible ? 'focus' : ''}`} > {props.children} </button> ); } import {useButton} from 'react-aria'; function Button(props) { let ref = useRef<HTMLButtonElement | null>(null); let { focusProps, isFocusVisible } = useFocusRing(); let { buttonProps } = useButton(props, ref); return ( <button {...mergeProps(buttonProps, focusProps)} ref={ref} className={`${props.className} ${ isFocusVisible ? 'focus' : '' }`} > {props.children} </button> ); } import {useButton} from 'react-aria'; function Button(props) { let ref = useRef< | HTMLButtonElement | null >(null); let { focusProps, isFocusVisible } = useFocusRing(); let { buttonProps } = useButton( props, ref ); return ( <button {...mergeProps( buttonProps, focusProps )} ref={ref} className={`${props.className} ${ isFocusVisible ? 'focus' : '' }`} > {props.children} </button> ); } ### Resizer# As described above, we need to implement an element that the user can drag/interact with to resize a column. Here we'll use the `useTableColumnResize` hook to create a visible resizer div for physical drag operations and a visually hidden input responsible for keyboard and screenreader interactions, similar to a slider. Users can press and drag on the visible resizer to trigger the `onResize` callbacks and update the tracked column widths accordingly. When focused, keyboard users can begin resizing the column by pressing Enter. Once resizing is activated, they can use the arrow keys to trigger the same resize events and press Enter, Esc, or Space to exit resizing. Touch screen reader users can swipe left or right to focus the column's resizer input and swipe up and down to resize the column. import {useTableColumnResize} from 'react-aria'; function Resizer(props) { let { column, layoutState, onResizeStart, onResize, onResizeEnd } = props; let ref = useRef<HTMLInputElement | null>(null); let { resizerProps, inputProps, isResizing } = useTableColumnResize( { column, 'aria-label': 'Resizer', onResizeStart, onResize, onResizeEnd }, layoutState, ref ); let { focusProps, isFocusVisible } = useFocusRing(); return ( <div role="presentation" className={`aria-table-resizer ${isFocusVisible ? 'focus' : ''} ${ isResizing ? 'resizing' : '' }`} {...resizerProps} > <input ref={ref} {...mergeProps(inputProps, focusProps)} /> </div> ); } import {useTableColumnResize} from 'react-aria'; function Resizer(props) { let { column, layoutState, onResizeStart, onResize, onResizeEnd } = props; let ref = useRef<HTMLInputElement | null>(null); let { resizerProps, inputProps, isResizing } = useTableColumnResize( { column, 'aria-label': 'Resizer', onResizeStart, onResize, onResizeEnd }, layoutState, ref ); let { focusProps, isFocusVisible } = useFocusRing(); return ( <div role="presentation" className={`aria-table-resizer ${ isFocusVisible ? 'focus' : '' } ${isResizing ? 'resizing' : ''}`} {...resizerProps} > <input ref={ref} {...mergeProps(inputProps, focusProps)} /> </div> ); } import {useTableColumnResize} from 'react-aria'; function Resizer(props) { let { column, layoutState, onResizeStart, onResize, onResizeEnd } = props; let ref = useRef< | HTMLInputElement | null >(null); let { resizerProps, inputProps, isResizing } = useTableColumnResize( { column, 'aria-label': 'Resizer', onResizeStart, onResize, onResizeEnd }, layoutState, ref ); let { focusProps, isFocusVisible } = useFocusRing(); return ( <div role="presentation" className={`aria-table-resizer ${ isFocusVisible ? 'focus' : '' } ${ isResizing ? 'resizing' : '' }`} {...resizerProps} > <input ref={ref} {...mergeProps( inputProps, focusProps )} /> </div> ); } Show CSS .aria-table-resizer { width: 15px; background-color: grey; cursor: col-resize; height: 30px; touch-action: none; flex: 0 0 auto; box-sizing: border-box; border: 5px; border-style: none solid; border-color: transparent; background-clip: content-box; } .aria-table-resizer.focus { background-color: orange; } .aria-table-resizer.resizing { border-color: orange; background-color: transparent; } .aria-table-resizer { width: 15px; background-color: grey; cursor: col-resize; height: 30px; touch-action: none; flex: 0 0 auto; box-sizing: border-box; border: 5px; border-style: none solid; border-color: transparent; background-clip: content-box; } .aria-table-resizer.focus { background-color: orange; } .aria-table-resizer.resizing { border-color: orange; background-color: transparent; } .aria-table-resizer { width: 15px; background-color: grey; cursor: col-resize; height: 30px; touch-action: none; flex: 0 0 auto; box-sizing: border-box; border: 5px; border-style: none solid; border-color: transparent; background-clip: content-box; } .aria-table-resizer.focus { background-color: orange; } .aria-table-resizer.resizing { border-color: orange; background-color: transparent; } And with that, all necessary changes to the previous table implementation have been made and we now have a table that supports resizable columns! The example below supports resizing via mouse, keyboard, touch, and screen reader interactions. To see an example with sorting and selection, see the styled example! <ResizableColumnsTable aria-label="Table with resizable columns"> <TableHeader> <Column allowsResizing>Name</Column> <Column allowsResizing>Type</Column> <Column allowsResizing>Level</Column> </TableHeader> <TableBody> <Row key="1"> <Cell>Charizard</Cell> <Cell>Fire, Flying</Cell> <Cell>67</Cell> </Row> <Row key="2"> <Cell>Blastoise</Cell> <Cell>Water</Cell> <Cell>56</Cell> </Row> <Row key="3"> <Cell>Venusaur</Cell> <Cell>Grass, Poison</Cell> <Cell>83</Cell> </Row> <Row key="4"> <Cell>Pikachu</Cell> <Cell>Electric</Cell> <Cell>100</Cell> </Row> </TableBody> </ResizableColumnsTable> <ResizableColumnsTable aria-label="Table with resizable columns"> <TableHeader> <Column allowsResizing>Name</Column> <Column allowsResizing>Type</Column> <Column allowsResizing>Level</Column> </TableHeader> <TableBody> <Row key="1"> <Cell>Charizard</Cell> <Cell>Fire, Flying</Cell> <Cell>67</Cell> </Row> <Row key="2"> <Cell>Blastoise</Cell> <Cell>Water</Cell> <Cell>56</Cell> </Row> <Row key="3"> <Cell>Venusaur</Cell> <Cell>Grass, Poison</Cell> <Cell>83</Cell> </Row> <Row key="4"> <Cell>Pikachu</Cell> <Cell>Electric</Cell> <Cell>100</Cell> </Row> </TableBody> </ResizableColumnsTable> <ResizableColumnsTable aria-label="Table with resizable columns"> <TableHeader> <Column allowsResizing > Name </Column> <Column allowsResizing > Type </Column> <Column allowsResizing > Level </Column> </TableHeader> <TableBody> <Row key="1"> <Cell> Charizard </Cell> <Cell> Fire, Flying </Cell> <Cell>67</Cell> </Row> <Row key="2"> <Cell> Blastoise </Cell> <Cell> Water </Cell> <Cell>56</Cell> </Row> <Row key="3"> <Cell> Venusaur </Cell> <Cell> Grass, Poison </Cell> <Cell>83</Cell> </Row> <Row key="4"> <Cell> Pikachu </Cell> <Cell> Electric </Cell> <Cell>100</Cell> </Row> </TableBody> </ResizableColumnsTable> | Name | Type | Level | | --- | --- | --- | | Charizard | Fire, Flying | 67 | | Blastoise | Water | 56 | | Venusaur | Grass, Poison | 83 | | Pikachu | Electric | 100 | ### Styled examples# Tailwind CSS A table supporting resizable columns, selection, and sorting built with Tailwind and React Aria. ## Internationalization# * * * `useTable` handles some aspects of internationalization automatically. For example, type to select is implemented with an Intl.Collator for internationalized string matching, and keyboard navigation is mirrored in right-to-left languages. You are responsible for localizing all text content within the table. ### RTL# In right-to-left languages, the table layout should be mirrored. The columns should be ordered from right to left and the individual column text alignment should be inverted. Ensure that your CSS accounts for this. --- ## Page: https://react-spectrum.adobe.com/react-aria/useTagGroup.html # useTagGroup Provides the behavior and accessibility implementation for a tag group component. A tag group is a focusable list of labels, categories, keywords, filters, or other items, with support for keyboard navigation, selection, and removal. <table><tbody><tr><th>install</th><td><code>yarn add react-aria</code></td></tr><tr><th>version</th><td>3.41.1</td></tr><tr><th>usage</th><td><code><span>import</span> {useTagGroup} <span>from</span> <span>'react-aria'</span></code></td></tr></tbody></table> View ARIA pattern W3C View repository GitHub View package NPM ## API# * * * `useTagGroup<T>( props: AriaTagGroupOptions <T>, state: ListState <T>, ref: RefObject <HTMLElement | | null> ): TagGroupAria ``useTag<T>( props: AriaTagProps <T>, state: ListState <T>, ref: RefObject <FocusableElement | | null> ): TagAria ` ## Features# * * * * Exposed to assistive technology as a grid using ARIA * Keyboard navigation support including arrow keys, home/end, page up/down, and delete * Keyboard focus management and cross browser normalization * Labeling support for accessibility * Support for mouse, touch, and keyboard interactions ## Anatomy# * * * A tag group consists of a list of tags. If a visual label is not provided, then an `aria-label` or `aria-labelledby` prop must be passed to identify the tag group to assistive technology. Individual tags should include a visual label, and may optionally include icons or a remove button. `useTagGroup` returns props for the group and its label, which you should spread onto the appropriate element: | Name | Type | Description | | --- | --- | --- | | `gridProps` | `DOMAttributes` | Props for the tag grouping element. | | `labelProps` | `DOMAttributes` | Props for the tag group's visible label (if any). | | `descriptionProps` | `DOMAttributes` | Props for the tag group description element, if any. | | `errorMessageProps` | `DOMAttributes` | Props for the tag group error message element, if any. | `useTag` returns props for an individual tag, along with states that you can use for styling: | Name | Type | Description | | --- | --- | --- | | `rowProps` | `DOMAttributes` | Props for the tag row element. | | `gridCellProps` | `DOMAttributes` | Props for the tag cell element. | | `removeButtonProps` | ` AriaButtonProps ` | Props for the tag remove button. | | `allowsRemoving` | `boolean` | Whether the tag can be removed. | | `isPressed` | `boolean` | Whether the item is currently in a pressed state. | | `isSelected` | `boolean` | Whether the item is currently selected. | | `isFocused` | `boolean` | Whether the item is currently focused. | | `isDisabled` | `boolean` | Whether the item is non-interactive, i.e. both selection and actions are disabled and the item may not be focused. Dependent on `disabledKeys` and `disabledBehavior`. | | `allowsSelection` | `boolean` | Whether the item may be selected, dependent on `selectionMode`, `disabledKeys`, and `disabledBehavior`. | In order to be correctly identified to assistive technologies and enable proper keyboard navigation, the tag group should use `gridProps` on its outer container. Each individual tag should use `rowProps` on its outer container, and use `gridCellProps` on an inner container. ## Example# * * * import type {ListState} from 'react-stately'; import type {AriaTagGroupProps, AriaTagProps} from 'react-aria'; import {Item, useListState} from 'react-stately'; import {useFocusRing, useTag, useTagGroup} from 'react-aria'; // Reuse the Button from your component library. See below for details. import {Button} from 'your-component-library'; function TagGroup<T extends object>(props: AriaTagGroupProps<T>) { let { label, description, errorMessage } = props; let ref = React.useRef(null); let state = useListState(props); let { gridProps, labelProps, descriptionProps, errorMessageProps } = useTagGroup(props, state, ref); return ( <div className="tag-group"> <div {...labelProps}>{label}</div> <div {...gridProps} ref={ref}> {[...state.collection].map((item) => ( <Tag key={item.key} item={item} state={state} /> ))} </div> {description && ( <div {...descriptionProps} className="description"> {description} </div> )} {errorMessage && ( <div {...errorMessageProps} className="error-message"> {errorMessage} </div> )} </div> ); } interface TagProps<T> extends AriaTagProps<T> { state: ListState<T>; } function Tag<T>(props: TagProps<T>) { let { item, state } = props; let ref = React.useRef(null); let { focusProps, isFocusVisible } = useFocusRing({ within: false }); let { rowProps, gridCellProps, removeButtonProps, allowsRemoving } = useTag( props, state, ref ); return ( <div ref={ref} {...rowProps} {...focusProps} data-focus-visible={isFocusVisible} > <div {...gridCellProps}> {item.rendered} {allowsRemoving && <Button {...removeButtonProps}>ⓧ</Button>} </div> </div> ); } <TagGroup label="Categories"> <Item key="news">News</Item> <Item key="travel">Travel</Item> <Item key="gaming">Gaming</Item> <Item key="shopping">Shopping</Item> </TagGroup> import type {ListState} from 'react-stately'; import type { AriaTagGroupProps, AriaTagProps } from 'react-aria'; import {Item, useListState} from 'react-stately'; import { useFocusRing, useTag, useTagGroup } from 'react-aria'; // Reuse the Button from your component library. See below for details. import {Button} from 'your-component-library'; function TagGroup<T extends object>( props: AriaTagGroupProps<T> ) { let { label, description, errorMessage } = props; let ref = React.useRef(null); let state = useListState(props); let { gridProps, labelProps, descriptionProps, errorMessageProps } = useTagGroup(props, state, ref); return ( <div className="tag-group"> <div {...labelProps}>{label}</div> <div {...gridProps} ref={ref}> {[...state.collection].map((item) => ( <Tag key={item.key} item={item} state={state} /> ))} </div> {description && ( <div {...descriptionProps} className="description"> {description} </div> )} {errorMessage && ( <div {...errorMessageProps} className="error-message" > {errorMessage} </div> )} </div> ); } interface TagProps<T> extends AriaTagProps<T> { state: ListState<T>; } function Tag<T>(props: TagProps<T>) { let { item, state } = props; let ref = React.useRef(null); let { focusProps, isFocusVisible } = useFocusRing({ within: false }); let { rowProps, gridCellProps, removeButtonProps, allowsRemoving } = useTag(props, state, ref); return ( <div ref={ref} {...rowProps} {...focusProps} data-focus-visible={isFocusVisible} > <div {...gridCellProps}> {item.rendered} {allowsRemoving && ( <Button {...removeButtonProps}>ⓧ</Button> )} </div> </div> ); } <TagGroup label="Categories"> <Item key="news">News</Item> <Item key="travel">Travel</Item> <Item key="gaming">Gaming</Item> <Item key="shopping">Shopping</Item> </TagGroup> import type {ListState} from 'react-stately'; import type { AriaTagGroupProps, AriaTagProps } from 'react-aria'; import { Item, useListState } from 'react-stately'; import { useFocusRing, useTag, useTagGroup } from 'react-aria'; // Reuse the Button from your component library. See below for details. import {Button} from 'your-component-library'; function TagGroup< T extends object >( props: AriaTagGroupProps<T> ) { let { label, description, errorMessage } = props; let ref = React.useRef( null ); let state = useListState(props); let { gridProps, labelProps, descriptionProps, errorMessageProps } = useTagGroup( props, state, ref ); return ( <div className="tag-group"> <div {...labelProps} > {label} </div> <div {...gridProps} ref={ref} > {[ ...state .collection ].map((item) => ( <Tag key={item .key} item={item} state={state} /> ))} </div> {description && ( <div {...descriptionProps} className="description" > {description} </div> )} {errorMessage && ( <div {...errorMessageProps} className="error-message" > {errorMessage} </div> )} </div> ); } interface TagProps<T> extends AriaTagProps<T> { state: ListState<T>; } function Tag<T>( props: TagProps<T> ) { let { item, state } = props; let ref = React.useRef( null ); let { focusProps, isFocusVisible } = useFocusRing({ within: false }); let { rowProps, gridCellProps, removeButtonProps, allowsRemoving } = useTag( props, state, ref ); return ( <div ref={ref} {...rowProps} {...focusProps} data-focus-visible={isFocusVisible} > <div {...gridCellProps} > {item.rendered} {allowsRemoving && ( <Button {...removeButtonProps} > ⓧ </Button> )} </div> </div> ); } <TagGroup label="Categories"> <Item key="news"> News </Item> <Item key="travel"> Travel </Item> <Item key="gaming"> Gaming </Item> <Item key="shopping"> Shopping </Item> </TagGroup> Categories News Travel Gaming Shopping Show CSS .tag-group { display: flex; flex-direction: column; gap: 4px; } .tag-group [role="grid"] { display: flex; flex-wrap: wrap; gap: 4px; } .tag-group [role="row"] { border: 1px solid gray; forced-color-adjust: none; border-radius: 4px; padding: 2px 8px; font-size: 0.929rem; outline: none; cursor: default; display: flex; align-items: center; transition: border-color 200ms; &[data-focus-visible=true] { outline: 2px solid slateblue; outline-offset: 2px; } &[aria-selected=true] { background: var(--spectrum-gray-900); border-color: var(--spectrum-gray-900); color: var(--spectrum-gray-50); } &[aria-disabled] { opacity: 0.4; } } .tag-group [role="gridcell"] { display: contents; } .tag-group [role="row"] button { background: none; border: none; padding: 0; margin-left: 4px; outline: none; font-size: 0.95em; border-radius: 100%; aspect-ratio: 1/1; height: 100%; &[data-focus-visible=true] { outline: 2px solid slateblue; outline-offset: -1px; } } .tag-group .description { font-size: 12px; } .tag-group .error-message { color: red; font-size: 12px; } .tag-group { display: flex; flex-direction: column; gap: 4px; } .tag-group [role="grid"] { display: flex; flex-wrap: wrap; gap: 4px; } .tag-group [role="row"] { border: 1px solid gray; forced-color-adjust: none; border-radius: 4px; padding: 2px 8px; font-size: 0.929rem; outline: none; cursor: default; display: flex; align-items: center; transition: border-color 200ms; &[data-focus-visible=true] { outline: 2px solid slateblue; outline-offset: 2px; } &[aria-selected=true] { background: var(--spectrum-gray-900); border-color: var(--spectrum-gray-900); color: var(--spectrum-gray-50); } &[aria-disabled] { opacity: 0.4; } } .tag-group [role="gridcell"] { display: contents; } .tag-group [role="row"] button { background: none; border: none; padding: 0; margin-left: 4px; outline: none; font-size: 0.95em; border-radius: 100%; aspect-ratio: 1/1; height: 100%; &[data-focus-visible=true] { outline: 2px solid slateblue; outline-offset: -1px; } } .tag-group .description { font-size: 12px; } .tag-group .error-message { color: red; font-size: 12px; } .tag-group { display: flex; flex-direction: column; gap: 4px; } .tag-group [role="grid"] { display: flex; flex-wrap: wrap; gap: 4px; } .tag-group [role="row"] { border: 1px solid gray; forced-color-adjust: none; border-radius: 4px; padding: 2px 8px; font-size: 0.929rem; outline: none; cursor: default; display: flex; align-items: center; transition: border-color 200ms; &[data-focus-visible=true] { outline: 2px solid slateblue; outline-offset: 2px; } &[aria-selected=true] { background: var(--spectrum-gray-900); border-color: var(--spectrum-gray-900); color: var(--spectrum-gray-50); } &[aria-disabled] { opacity: 0.4; } } .tag-group [role="gridcell"] { display: contents; } .tag-group [role="row"] button { background: none; border: none; padding: 0; margin-left: 4px; outline: none; font-size: 0.95em; border-radius: 100%; aspect-ratio: 1/1; height: 100%; &[data-focus-visible=true] { outline: 2px solid slateblue; outline-offset: -1px; } } .tag-group .description { font-size: 12px; } .tag-group .error-message { color: red; font-size: 12px; } ### Button# The `Button` component is used in the above example to remove a tag. It is built using the useButton hook, and can be shared with many other components. Show code import {mergeProps, useButton} from 'react-aria'; function Button(props) { let ref = React.useRef(null); let { buttonProps } = useButton(props, ref); let { focusProps, isFocusVisible } = useFocusRing({ within: false }); return ( <button {...mergeProps(buttonProps, focusProps)} ref={ref} data-focus-visible={isFocusVisible} > {props.children} </button> ); } import {mergeProps, useButton} from 'react-aria'; function Button(props) { let ref = React.useRef(null); let { buttonProps } = useButton(props, ref); let { focusProps, isFocusVisible } = useFocusRing({ within: false }); return ( <button {...mergeProps(buttonProps, focusProps)} ref={ref} data-focus-visible={isFocusVisible} > {props.children} </button> ); } import { mergeProps, useButton } from 'react-aria'; function Button(props) { let ref = React.useRef( null ); let { buttonProps } = useButton( props, ref ); let { focusProps, isFocusVisible } = useFocusRing({ within: false }); return ( <button {...mergeProps( buttonProps, focusProps )} ref={ref} data-focus-visible={isFocusVisible} > {props.children} </button> ); } ## Styled examples# * * * Tailwind CSS A TagGroup built with Tailwind. ## Usage# * * * ### Remove tags# The `onRemove` prop can be used to include a remove button which can be used to remove a tag. This allows the user to press the remove button, or press the backspace key while the tag is focused to remove the tag from the group. Additionally, when selection is enabled, all selected items will be deleted when pressing the backspace key on a selected tag. import {useListData} from 'react-stately'; function Example() { let list = useListData({ initialItems: [ { id: 1, name: 'News' }, { id: 2, name: 'Travel' }, { id: 3, name: 'Gaming' }, { id: 4, name: 'Shopping' } ] }); return ( <TagGroup label="Categories" items={list.items} onRemove={(keys) => list.remove(...keys)} > {(item) => <Item>{item.name}</Item>} </TagGroup> ); } import {useListData} from 'react-stately'; function Example() { let list = useListData({ initialItems: [ { id: 1, name: 'News' }, { id: 2, name: 'Travel' }, { id: 3, name: 'Gaming' }, { id: 4, name: 'Shopping' } ] }); return ( <TagGroup label="Categories" items={list.items} onRemove={(keys) => list.remove(...keys)} > {(item) => <Item>{item.name}</Item>} </TagGroup> ); } import {useListData} from 'react-stately'; function Example() { let list = useListData( { initialItems: [ { id: 1, name: 'News' }, { id: 2, name: 'Travel' }, { id: 3, name: 'Gaming' }, { id: 4, name: 'Shopping' } ] } ); return ( <TagGroup label="Categories" items={list.items} onRemove={(keys) => list.remove( ...keys )} > {(item) => ( <Item> {item.name} </Item> )} </TagGroup> ); } Categories Newsⓧ Travelⓧ Gamingⓧ Shoppingⓧ ### Selection# TagGroup supports multiple selection modes. By default, selection is disabled, however this can be changed using the `selectionMode` prop. Use `defaultSelectedKeys` to provide a default set of selected items (uncontrolled) and `selectedKeys` to set the selected items (controlled). The value of the selected keys must match the `key` prop of the items. See the `react-stately` Selection docs for more details. import type {Selection} from 'react-stately'; function Example() { let [selected, setSelected] = React.useState<Selection>(new Set(['parking'])); return ( <> <TagGroup label="Amenities" selectionMode="multiple" selectedKeys={selected} onSelectionChange={setSelected} > <Item key="laundry">Laundry</Item> <Item key="fitness">Fitness center</Item> <Item key="parking">Parking</Item> <Item key="pool">Swimming pool</Item> <Item key="breakfast">Breakfast</Item> </TagGroup> <p> Current selection (controlled):{' '} {selected === 'all' ? 'all' : [...selected].join(', ')} </p> </> ); } import type {Selection} from 'react-stately'; function Example() { let [selected, setSelected] = React.useState<Selection>( new Set(['parking']) ); return ( <> <TagGroup label="Amenities" selectionMode="multiple" selectedKeys={selected} onSelectionChange={setSelected} > <Item key="laundry">Laundry</Item> <Item key="fitness">Fitness center</Item> <Item key="parking">Parking</Item> <Item key="pool">Swimming pool</Item> <Item key="breakfast">Breakfast</Item> </TagGroup> <p> Current selection (controlled): {selected === 'all' ? 'all' : [...selected].join(', ')} </p> </> ); } import type {Selection} from 'react-stately'; function Example() { let [ selected, setSelected ] = React.useState< Selection >( new Set(['parking']) ); return ( <> <TagGroup label="Amenities" selectionMode="multiple" selectedKeys={selected} onSelectionChange={setSelected} > <Item key="laundry"> Laundry </Item> <Item key="fitness"> Fitness center </Item> <Item key="parking"> Parking </Item> <Item key="pool"> Swimming pool </Item> <Item key="breakfast"> Breakfast </Item> </TagGroup> <p> Current selection (controlled): {' '} {selected === 'all' ? 'all' : [...selected] .join(', ')} </p> </> ); } Amenities Laundry Fitness center Parking Swimming pool Breakfast Current selection (controlled): parking ### Links# Tags may be links to another page or website. This can be achieved by passing the `href` prop to the `<Item>` component. Tags with an `href` are not selectable. <TagGroup label="Links"> <Item href="https://adobe.com/" target="_blank">Adobe</Item> <Item href="https://apple.com/" target="_blank">Apple</Item> <Item href="https://google.com/" target="_blank">Google</Item> <Item href="https://microsoft.com/" target="_blank">Microsoft</Item> </TagGroup> <TagGroup label="Links"> <Item href="https://adobe.com/" target="_blank"> Adobe </Item> <Item href="https://apple.com/" target="_blank"> Apple </Item> <Item href="https://google.com/" target="_blank"> Google </Item> <Item href="https://microsoft.com/" target="_blank"> Microsoft </Item> </TagGroup> <TagGroup label="Links"> <Item href="https://adobe.com/" target="_blank" > Adobe </Item> <Item href="https://apple.com/" target="_blank" > Apple </Item> <Item href="https://google.com/" target="_blank" > Google </Item> <Item href="https://microsoft.com/" target="_blank" > Microsoft </Item> </TagGroup> Links Adobe Apple Google Microsoft #### Client side routing# The `<Item>` component works with frameworks and client side routers like Next.js and React Router. As with other React Aria components that support links, this works via the `RouterProvider` component at the root of your app. See the client side routing guide to learn how to set this up. ### Disabled tags# TagGroup supports marking items as disabled using the `disabledKeys` prop. Each key in this list corresponds with the `key` prop passed to the `Item` component, or automatically derived from the values passed to the `items` prop. Disabled items are not focusable, selectable, or keyboard navigable. See Collections for more details. <TagGroup label="Sandwich contents" selectionMode="multiple" disabledKeys={['tuna']} > <Item key="lettuce">Lettuce</Item> <Item key="tomato">Tomato</Item> <Item key="cheese">Cheese</Item> <Item key="tuna">Tuna Salad</Item> <Item key="egg">Egg Salad</Item> <Item key="ham">Ham</Item> </TagGroup> <TagGroup label="Sandwich contents" selectionMode="multiple" disabledKeys={['tuna']} > <Item key="lettuce">Lettuce</Item> <Item key="tomato">Tomato</Item> <Item key="cheese">Cheese</Item> <Item key="tuna">Tuna Salad</Item> <Item key="egg">Egg Salad</Item> <Item key="ham">Ham</Item> </TagGroup> <TagGroup label="Sandwich contents" selectionMode="multiple" disabledKeys={[ 'tuna' ]} > <Item key="lettuce"> Lettuce </Item> <Item key="tomato"> Tomato </Item> <Item key="cheese"> Cheese </Item> <Item key="tuna"> Tuna Salad </Item> <Item key="egg"> Egg Salad </Item> <Item key="ham"> Ham </Item> </TagGroup> Sandwich contents Lettuce Tomato Cheese Tuna Salad Egg Salad Ham ### Description# The `description` prop can be used to associate additional help text with a tag group. <TagGroup label="Categories" description="Your selected categories."> <Item key="news">News</Item> <Item key="travel">Travel</Item> <Item key="gaming">Gaming</Item> <Item key="shopping">Shopping</Item> </TagGroup> <TagGroup label="Categories" description="Your selected categories." > <Item key="news">News</Item> <Item key="travel">Travel</Item> <Item key="gaming">Gaming</Item> <Item key="shopping">Shopping</Item> </TagGroup> <TagGroup label="Categories" description="Your selected categories." > <Item key="news"> News </Item> <Item key="travel"> Travel </Item> <Item key="gaming"> Gaming </Item> <Item key="shopping"> Shopping </Item> </TagGroup> Categories News Travel Gaming Shopping Your selected categories. ### Error message# The `errorMessage` prop can be used to help the user fix a validation error. <TagGroup label="Categories" errorMessage="Invalid set of categories."> <Item key="news">News</Item> <Item key="travel">Travel</Item> <Item key="gaming">Gaming</Item> <Item key="shopping">Shopping</Item> </TagGroup> <TagGroup label="Categories" errorMessage="Invalid set of categories." > <Item key="news">News</Item> <Item key="travel">Travel</Item> <Item key="gaming">Gaming</Item> <Item key="shopping">Shopping</Item> </TagGroup> <TagGroup label="Categories" errorMessage="Invalid set of categories." > <Item key="news"> News </Item> <Item key="travel"> Travel </Item> <Item key="gaming"> Gaming </Item> <Item key="shopping"> Shopping </Item> </TagGroup> Categories News Travel Gaming Shopping Invalid set of categories. --- ## Page: https://react-spectrum.adobe.com/react-aria/useColorArea.html # useColorArea Provides the behavior and accessibility implementation for a color area component. Color area allows users to adjust two channels of an RGB, HSL or HSB color value against a two-dimensional gradient background. <table><tbody><tr><th>install</th><td><code>yarn add react-aria</code></td></tr><tr><th>version</th><td>3.41.1</td></tr><tr><th>usage</th><td><code><span>import</span> {useColorArea} <span>from</span> <span>'react-aria'</span></code></td></tr></tbody></table> View ARIA pattern W3C View repository GitHub View package NPM ## API# * * * `useColorArea( (props: AriaColorAreaOptions , , state: ColorAreaState )): ColorAreaAria ` ## Features# * * * The <input type="color"> HTML element can be used to build a color picker, however it is very inconsistent across browsers and operating systems and consists of a complete color picker rather than a color area. `useColorArea` helps achieve accessible and touch-friendly color areas that can be styled as needed. * Support for adjusting two-channel values of an HSL, HSB or RGB color value * Support for mouse, touch, and keyboard via the useMove hook * Multi-touch support * Pressing on the color area background moves the thumb to that position * Supports using the arrow keys, for changing value by step, as well as shift + arrow key, page up/down, home, and end keys, for changing the value by page step. * Support for disabling the color area * Prevents text selection while dragging * Exposed to assistive technology as a `2D slider` element via ARIA * Uses two hidden native input elements within a group to support touch screen readers * Automatic ARIA labeling using the localized channel names by default * Support for mirroring in RTL locales ## Anatomy# * * * A color area consists of a rectangular background area that provides, using a two-dimensional gradient, a visual representation of the range of color values from which a user can select, and a thumb that the user can drag to change the selected color value. Two visually hidden `<input>` elements are used to represent the color channel values to assistive technologies. `useColorArea` returns five sets of props that you should spread onto the appropriate elements: | Name | Type | Description | | --- | --- | --- | | `colorAreaProps` | `DOMAttributes` | Props for the color area container element. | | `thumbProps` | `DOMAttributes` | Props for the thumb element. | | `xInputProps` | `InputHTMLAttributes<HTMLInputElement>` | Props for the visually hidden horizontal range input element. | | `yInputProps` | `InputHTMLAttributes<HTMLInputElement>` | Props for the visually hidden vertical range input element. | State is managed by the `useColorAreaState` hook from `@react-stately/color`. The state object should be passed as an option to `useColorArea`. By default, `useColorArea` provides an `aria-label` for the localized string "Color Picker", which labels the visually hidden `<input>` elements for the two color channels, or on mobile devices, the group containing them. If you wish to override this with a more specific label, an `aria-label` or `aria-labelledby` prop may be passed to further to identify the element to assistive technologies. The `aria-valuetext` for each `<input>` will include the localized color channel name and current value for each channel. ## Example# * * * This example shows how to build a color area with a draggable thumb to adjust two color channel values of a color. Styling for the background gradient and positioning of the thumb are provided by `useColorArea` in the returned props for each element. The two `<input>` elements inside the thumb represent the color channel values to assistive technologies, and are hidden from view. The thumb also uses the useFocusRing hook to grow in size when it is keyboard focused (try tabbing to it). import {useColorAreaState} from 'react-stately'; import {useColorArea, useFocusRing} from 'react-aria'; const SIZE = 192; const FOCUSED_THUMB_SIZE = 28; const THUMB_SIZE = 20; const BORDER_RADIUS = 4; function ColorArea(props) { let inputXRef = React.useRef(null); let inputYRef = React.useRef(null); let containerRef = React.useRef(null); let state = useColorAreaState(props); let { isDisabled } = props; let { colorAreaProps, xInputProps, yInputProps, thumbProps } = useColorArea({ ...props, inputXRef, inputYRef, containerRef }, state); let { focusProps, isFocusVisible } = useFocusRing(); return ( <div ref={containerRef} {...colorAreaProps} style={{ ...colorAreaProps.style, width: SIZE, height: SIZE, borderRadius: BORDER_RADIUS, background: isDisabled ? 'rgb(142, 142, 142)' : colorAreaProps.style.background, opacity: isDisabled ? 0.3 : undefined }} > <div {...thumbProps} style={{ ...thumbProps.style, background: isDisabled ? 'rgb(142, 142, 142)' : state.getDisplayColor().toString('css'), border: `2px solid ${isDisabled ? 'rgb(142, 142, 142)' : 'white'}`, borderRadius: '50%', boxShadow: '0 0 0 1px black, inset 0 0 0 1px black', boxSizing: 'border-box', height: isFocusVisible ? FOCUSED_THUMB_SIZE + 4 : THUMB_SIZE, width: isFocusVisible ? FOCUSED_THUMB_SIZE + 4 : THUMB_SIZE }} > <input ref={inputXRef} {...xInputProps} {...focusProps} /> <input ref={inputYRef} {...yInputProps} {...focusProps} /> </div> </div> ); } <ColorArea /> import {useColorAreaState} from 'react-stately'; import {useColorArea, useFocusRing} from 'react-aria'; const SIZE = 192; const FOCUSED_THUMB_SIZE = 28; const THUMB_SIZE = 20; const BORDER_RADIUS = 4; function ColorArea(props) { let inputXRef = React.useRef(null); let inputYRef = React.useRef(null); let containerRef = React.useRef(null); let state = useColorAreaState(props); let { isDisabled } = props; let { colorAreaProps, xInputProps, yInputProps, thumbProps } = useColorArea({ ...props, inputXRef, inputYRef, containerRef }, state); let { focusProps, isFocusVisible } = useFocusRing(); return ( <div ref={containerRef} {...colorAreaProps} style={{ ...colorAreaProps.style, width: SIZE, height: SIZE, borderRadius: BORDER_RADIUS, background: isDisabled ? 'rgb(142, 142, 142)' : colorAreaProps.style.background, opacity: isDisabled ? 0.3 : undefined }} > <div {...thumbProps} style={{ ...thumbProps.style, background: isDisabled ? 'rgb(142, 142, 142)' : state.getDisplayColor().toString('css'), border: `2px solid ${ isDisabled ? 'rgb(142, 142, 142)' : 'white' }`, borderRadius: '50%', boxShadow: '0 0 0 1px black, inset 0 0 0 1px black', boxSizing: 'border-box', height: isFocusVisible ? FOCUSED_THUMB_SIZE + 4 : THUMB_SIZE, width: isFocusVisible ? FOCUSED_THUMB_SIZE + 4 : THUMB_SIZE }} > <input ref={inputXRef} {...xInputProps} {...focusProps} /> <input ref={inputYRef} {...yInputProps} {...focusProps} /> </div> </div> ); } <ColorArea /> import {useColorAreaState} from 'react-stately'; import { useColorArea, useFocusRing } from 'react-aria'; const SIZE = 192; const FOCUSED_THUMB_SIZE = 28; const THUMB_SIZE = 20; const BORDER_RADIUS = 4; function ColorArea( props ) { let inputXRef = React .useRef(null); let inputYRef = React .useRef(null); let containerRef = React.useRef(null); let state = useColorAreaState( props ); let { isDisabled } = props; let { colorAreaProps, xInputProps, yInputProps, thumbProps } = useColorArea({ ...props, inputXRef, inputYRef, containerRef }, state); let { focusProps, isFocusVisible } = useFocusRing(); return ( <div ref={containerRef} {...colorAreaProps} style={{ ...colorAreaProps .style, width: SIZE, height: SIZE, borderRadius: BORDER_RADIUS, background: isDisabled ? 'rgb(142, 142, 142)' : colorAreaProps .style .background, opacity: isDisabled ? 0.3 : undefined }} > <div {...thumbProps} style={{ ...thumbProps .style, background: isDisabled ? 'rgb(142, 142, 142)' : state .getDisplayColor() .toString( 'css' ), border: `2px solid ${ isDisabled ? 'rgb(142, 142, 142)' : 'white' }`, borderRadius: '50%', boxShadow: '0 0 0 1px black, inset 0 0 0 1px black', boxSizing: 'border-box', height: isFocusVisible ? FOCUSED_THUMB_SIZE + 4 : THUMB_SIZE, width: isFocusVisible ? FOCUSED_THUMB_SIZE + 4 : THUMB_SIZE }} > <input ref={inputXRef} {...xInputProps} {...focusProps} /> <input ref={inputYRef} {...yInputProps} {...focusProps} /> </div> </div> ); } <ColorArea /> ## Usage# * * * The following examples show how to use the `ColorArea` component created in the above example. ### Uncontrolled# By default, color area is uncontrolled, with a default value of white using the RGB color space (`rgb(255, 255, 255)`). You can change the default value using the `defaultValue` prop, and the color area will use the color space of the provided value. If no `xChannel` or `yChannel` is provided, for the RGB color space, the `red` color channel maps to the horizontal axis or `xChannel`, and the `green` color channel maps to the vertical axis or `yChannel`. Similarly, for the HSL and HSB color spaces, the `hue` color channel maps to the horizontal axis or `xChannel`, and the `saturation` color channel maps to the vertical axis or `yChannel`. <label id="hsb-label-id">x: Hue, y: Saturation</label> <ColorArea aria-labelledby="hsb-label-id" defaultValue="hsb(219, 58%, 93%)" /> <label id="hsb-label-id">x: Hue, y: Saturation</label> <ColorArea aria-labelledby="hsb-label-id" defaultValue="hsb(219, 58%, 93%)" /> <label id="hsb-label-id"> x: Hue, y: Saturation </label> <ColorArea aria-labelledby="hsb-label-id" defaultValue="hsb(219, 58%, 93%)" /> x: Hue, y: Saturation ### Controlled# A color area can be made controlled using the `value` prop. The `parseColor` function is used to parse the initial color from an RGB, HSL or HSB string, stored in state. The `onChange` prop is used to update the value in the state when the user drags the thumb. This is the more common usage because it allows to adjust the third color channel using a separate control, like a color slider using the useColorSlider hook or a color wheel using the useColorWheel hook, or to display the color value stored in a state using a preview swatch. The `onChangeEnd` prop can be used to handle when a user stops dragging the color area. import {parseColor} from 'react-stately'; function Example() { let [ color, setColor ] = React.useState(parseColor('hsba(219, 58%, 93%, 0.75)')); let [ endColor, setEndColor ] = React.useState(color); let [ xChannel, yChannel, zChannel ] = color.getColorChannels(); return ( <> <label id="hsb-label-id-1"> x: {color.getChannelName(xChannel, 'en-US')}, y:{' '} {color.getChannelName(yChannel, 'en-US')} </label> <div style={{ display: 'flex', flexWrap: 'wrap', gap: '1rem' }} > <div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }} > <ColorArea aria-labelledby="hsb-label-id-1" value={color} onChange={setColor} onChangeEnd={setEndColor} xChannel={xChannel} yChannel={yChannel} /> <ColorSlider channel={zChannel} value={color} onChange={setColor} onChangeEnd={setEndColor} /> <ColorSlider channel="alpha" value={color} onChange={setColor} onChangeEnd={setEndColor} /> </div> <div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }} > <div style={{ display: 'flex', gap: '6px' }} > <ColorSwatch color={color.withChannelValue('alpha', 1)} aria-label={`current color swatch: ${color.toString('hsl')}`} /> <ColorSwatch color={color} aria-label={`current color swatch with alpha channel: ${ color.toString('hsla') }`} /> </div> <div style={{ display: 'flex', gap: '6px' }} > <ColorSwatch color={endColor.withChannelValue('alpha', 1)} aria-label={`end color swatch: ${endColor.toString('hsb')}`} /> <ColorSwatch color={endColor} aria-label={`end color swatch with alpha channel: ${ endColor.toString('hsba') }`} /> </div> </div> </div> <p>Current color value: {color.toString('hsba')}</p> <p>End color value: {endColor.toString('hsba')}</p> </> ); } import {parseColor} from 'react-stately'; function Example() { let [ color, setColor ] = React.useState( parseColor('hsba(219, 58%, 93%, 0.75)') ); let [ endColor, setEndColor ] = React.useState(color); let [ xChannel, yChannel, zChannel ] = color.getColorChannels(); return ( <> <label id="hsb-label-id-1"> x: {color.getChannelName(xChannel, 'en-US')}, y: {' '} {color.getChannelName(yChannel, 'en-US')} </label> <div style={{ display: 'flex', flexWrap: 'wrap', gap: '1rem' }} > <div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }} > <ColorArea aria-labelledby="hsb-label-id-1" value={color} onChange={setColor} onChangeEnd={setEndColor} xChannel={xChannel} yChannel={yChannel} /> <ColorSlider channel={zChannel} value={color} onChange={setColor} onChangeEnd={setEndColor} /> <ColorSlider channel="alpha" value={color} onChange={setColor} onChangeEnd={setEndColor} /> </div> <div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }} > <div style={{ display: 'flex', gap: '6px' }} > <ColorSwatch color={color.withChannelValue('alpha', 1)} aria-label={`current color swatch: ${ color.toString('hsl') }`} /> <ColorSwatch color={color} aria-label={`current color swatch with alpha channel: ${ color.toString('hsla') }`} /> </div> <div style={{ display: 'flex', gap: '6px' }} > <ColorSwatch color={endColor.withChannelValue('alpha', 1)} aria-label={`end color swatch: ${ endColor.toString('hsb') }`} /> <ColorSwatch color={endColor} aria-label={`end color swatch with alpha channel: ${ endColor.toString('hsba') }`} /> </div> </div> </div> <p>Current color value: {color.toString('hsba')}</p> <p>End color value: {endColor.toString('hsba')}</p> </> ); } import {parseColor} from 'react-stately'; function Example() { let [ color, setColor ] = React.useState( parseColor( 'hsba(219, 58%, 93%, 0.75)' ) ); let [ endColor, setEndColor ] = React.useState( color ); let [ xChannel, yChannel, zChannel ] = color .getColorChannels(); return ( <> <label id="hsb-label-id-1"> x:{' '} {color .getChannelName( xChannel, 'en-US' )}, y:{' '} {color .getChannelName( yChannel, 'en-US' )} </label> <div style={{ display: 'flex', flexWrap: 'wrap', gap: '1rem' }} > <div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }} > <ColorArea aria-labelledby="hsb-label-id-1" value={color} onChange={setColor} onChangeEnd={setEndColor} xChannel={xChannel} yChannel={yChannel} /> <ColorSlider channel={zChannel} value={color} onChange={setColor} onChangeEnd={setEndColor} /> <ColorSlider channel="alpha" value={color} onChange={setColor} onChangeEnd={setEndColor} /> </div> <div style={{ display: 'flex', flexDirection: 'column', gap: '1rem' }} > <div style={{ display: 'flex', gap: '6px' }} > <ColorSwatch color={color .withChannelValue( 'alpha', 1 )} aria-label={`current color swatch: ${ color .toString( 'hsl' ) }`} /> <ColorSwatch color={color} aria-label={`current color swatch with alpha channel: ${ color .toString( 'hsla' ) }`} /> </div> <div style={{ display: 'flex', gap: '6px' }} > <ColorSwatch color={endColor .withChannelValue( 'alpha', 1 )} aria-label={`end color swatch: ${ endColor .toString( 'hsb' ) }`} /> <ColorSwatch color={endColor} aria-label={`end color swatch with alpha channel: ${ endColor .toString( 'hsba' ) }`} /> </div> </div> </div> <p> Current color value:{' '} {color.toString( 'hsba' )} </p> <p> End color value: {' '} {endColor .toString( 'hsba' )} </p> </> ); } x: Hue, y: Saturation Brightness 93% Alpha 75% Current color value: hsba(219, 58%, 93%, 0.75) End color value: hsba(219, 58%, 93%, 0.75) ### ColorSlider# The `ColorSlider` component used in the example above controls the channel value not controlled by the `ColorArea`, in this case, the `brightness` channel, or the `alpha` channel. It is built using the useColorSlider hook, and can be shared with other color components. Show code import {useColorSliderState} from 'react-stately'; import {useColorSlider, useFocusRing, useLocale, VisuallyHidden} from 'react-aria'; function ColorSlider(props) { let { locale } = useLocale(); let state = useColorSliderState({ ...props, locale }); let trackRef = React.useRef(null); let inputRef = React.useRef(null); // Default label to the channel name in the current locale let label = props.label || state.value.getChannelName(props.channel, locale); let { trackProps, thumbProps, inputProps, labelProps, outputProps } = useColorSlider({ ...props, label, trackRef, inputRef }, state); let { focusProps, isFocusVisible } = useFocusRing(); return ( <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', width: SIZE }} > {/* Create a flex container for the label and output element. */} <div style={{ display: 'flex', alignSelf: 'stretch' }}> <label {...labelProps}>{label}</label> <output {...outputProps} style={{ flex: '1 0 auto', textAlign: 'end' }}> {state.value.formatChannelValue(props.channel, locale)} </output> </div> {/* The track element holds the visible track line and the thumb. */} <div className="color-slider-track" {...trackProps} ref={trackRef} style={{ height: FOCUSED_THUMB_SIZE }} > <div className="color-slider-track-background"></div> <div className="color-slider-track-color" style={{ ...trackProps.style }} > </div> <div className={`color-slider-thumb${isFocusVisible ? ' is-focused' : ''}`} {...thumbProps} style={{ ...thumbProps.style }} > <div className="color-slider-thumb-background"></div> <div className="color-slider-thumb-color" style={{ background: state.getDisplayColor().toString('css') }} > </div> <VisuallyHidden> <input ref={inputRef} {...inputProps} {...focusProps} /> </VisuallyHidden> </div> </div> </div> ); } import {useColorSliderState} from 'react-stately'; import { useColorSlider, useFocusRing, useLocale, VisuallyHidden } from 'react-aria'; function ColorSlider(props) { let { locale } = useLocale(); let state = useColorSliderState({ ...props, locale }); let trackRef = React.useRef(null); let inputRef = React.useRef(null); // Default label to the channel name in the current locale let label = props.label || state.value.getChannelName(props.channel, locale); let { trackProps, thumbProps, inputProps, labelProps, outputProps } = useColorSlider({ ...props, label, trackRef, inputRef }, state); let { focusProps, isFocusVisible } = useFocusRing(); return ( <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', width: SIZE }} > {/* Create a flex container for the label and output element. */} <div style={{ display: 'flex', alignSelf: 'stretch' }} > <label {...labelProps}>{label}</label> <output {...outputProps} style={{ flex: '1 0 auto', textAlign: 'end' }} > {state.value.formatChannelValue( props.channel, locale )} </output> </div> {/* The track element holds the visible track line and the thumb. */} <div className="color-slider-track" {...trackProps} ref={trackRef} style={{ height: FOCUSED_THUMB_SIZE }} > <div className="color-slider-track-background"> </div> <div className="color-slider-track-color" style={{ ...trackProps.style }} > </div> <div className={`color-slider-thumb${ isFocusVisible ? ' is-focused' : '' }`} {...thumbProps} style={{ ...thumbProps.style }} > <div className="color-slider-thumb-background"> </div> <div className="color-slider-thumb-color" style={{ background: state.getDisplayColor().toString( 'css' ) }} > </div> <VisuallyHidden> <input ref={inputRef} {...inputProps} {...focusProps} /> </VisuallyHidden> </div> </div> </div> ); } import {useColorSliderState} from 'react-stately'; import { useColorSlider, useFocusRing, useLocale, VisuallyHidden } from 'react-aria'; function ColorSlider( props ) { let { locale } = useLocale(); let state = useColorSliderState({ ...props, locale }); let trackRef = React .useRef(null); let inputRef = React .useRef(null); // Default label to the channel name in the current locale let label = props.label || state.value .getChannelName( props.channel, locale ); let { trackProps, thumbProps, inputProps, labelProps, outputProps } = useColorSlider({ ...props, label, trackRef, inputRef }, state); let { focusProps, isFocusVisible } = useFocusRing(); return ( <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', width: SIZE }} > {/* Create a flex container for the label and output element. */} <div style={{ display: 'flex', alignSelf: 'stretch' }} > <label {...labelProps} > {label} </label> <output {...outputProps} style={{ flex: '1 0 auto', textAlign: 'end' }} > {state.value .formatChannelValue( props .channel, locale )} </output> </div> {/* The track element holds the visible track line and the thumb. */} <div className="color-slider-track" {...trackProps} ref={trackRef} style={{ height: FOCUSED_THUMB_SIZE }} > <div className="color-slider-track-background"> </div> <div className="color-slider-track-color" style={{ ...trackProps .style }} > </div> <div className={`color-slider-thumb${ isFocusVisible ? ' is-focused' : '' }`} {...thumbProps} style={{ ...thumbProps .style }} > <div className="color-slider-thumb-background"> </div> <div className="color-slider-thumb-color" style={{ background: state .getDisplayColor() .toString( 'css' ) }} > </div> <VisuallyHidden> <input ref={inputRef} {...inputProps} {...focusProps} /> </VisuallyHidden> </div> </div> </div> ); } Show CSS .color-slider-track, .color-slider-track-background, .color-slider-track-color { width: 100%; border-radius: 4px; forced-color-adjust: none; position: relative; } .color-slider-track-background, .color-slider-track-color { position: absolute; height: 100%; } .color-slider-thumb { position: absolute; top: 14px; border: 2px solid white; box-shadow: 0 0 0 1px black, inset 0 0 0 1px black; border-radius: 50%; box-sizing: border-box; width: 20px; height: 20px; } .color-slider-thumb.is-focused { width: 32px; height: 32px; } .color-slider-track-background, .color-slider-thumb-background { background-size: 16px 16px; background-position: -2px -2px, -2px 6px, 6px -10px, -10px -2px; background-color: white; background-image: linear-gradient(-45deg, transparent 75.5%, rgb(188, 188, 188) 75.5%), linear-gradient(45deg, transparent 75.5%, rgb(188, 188, 188) 75.5%), linear-gradient(-45deg, rgb(188, 188, 188) 25.5%, transparent 25.5%), linear-gradient(45deg, rgb(188, 188, 188) 25.5%, transparent 25.5%); } .color-slider-thumb-background, .color-slider-thumb-color { position: absolute; border-radius: 50%; width: 100%; height: 100%; } .color-slider-track, .color-slider-track-background, .color-slider-track-color { width: 100%; border-radius: 4px; forced-color-adjust: none; position: relative; } .color-slider-track-background, .color-slider-track-color { position: absolute; height: 100%; } .color-slider-thumb { position: absolute; top: 14px; border: 2px solid white; box-shadow: 0 0 0 1px black, inset 0 0 0 1px black; border-radius: 50%; box-sizing: border-box; width: 20px; height: 20px; } .color-slider-thumb.is-focused { width: 32px; height: 32px; } .color-slider-track-background, .color-slider-thumb-background { background-size: 16px 16px; background-position: -2px -2px, -2px 6px, 6px -10px, -10px -2px; background-color: white; background-image: linear-gradient(-45deg, transparent 75.5%, rgb(188, 188, 188) 75.5%), linear-gradient(45deg, transparent 75.5%, rgb(188, 188, 188) 75.5%), linear-gradient(-45deg, rgb(188, 188, 188) 25.5%, transparent 25.5%), linear-gradient(45deg, rgb(188, 188, 188) 25.5%, transparent 25.5%); } .color-slider-thumb-background, .color-slider-thumb-color { position: absolute; border-radius: 50%; width: 100%; height: 100%; } .color-slider-track, .color-slider-track-background, .color-slider-track-color { width: 100%; border-radius: 4px; forced-color-adjust: none; position: relative; } .color-slider-track-background, .color-slider-track-color { position: absolute; height: 100%; } .color-slider-thumb { position: absolute; top: 14px; border: 2px solid white; box-shadow: 0 0 0 1px black, inset 0 0 0 1px black; border-radius: 50%; box-sizing: border-box; width: 20px; height: 20px; } .color-slider-thumb.is-focused { width: 32px; height: 32px; } .color-slider-track-background, .color-slider-thumb-background { background-size: 16px 16px; background-position: -2px -2px, -2px 6px, 6px -10px, -10px -2px; background-color: white; background-image: linear-gradient(-45deg, transparent 75.5%, rgb(188, 188, 188) 75.5%), linear-gradient(45deg, transparent 75.5%, rgb(188, 188, 188) 75.5%), linear-gradient(-45deg, rgb(188, 188, 188) 25.5%, transparent 25.5%), linear-gradient(45deg, rgb(188, 188, 188) 25.5%, transparent 25.5%); } .color-slider-thumb-background, .color-slider-thumb-color { position: absolute; border-radius: 50%; width: 100%; height: 100%; } ### ColorSwatch# The `ColorSwatch` component used in the example above implements an image preview of the color with the useColorSwatch hook. Show code import {useColorSwatch} from 'react-aria'; function ColorSwatch(props) { let { colorSwatchProps, color } = useColorSwatch(props); return ( <span {...colorSwatchProps} style={{ ...colorSwatchProps.style, display: 'inline-block', width: 32, height: 32, borderRadius: 4, background: `linear-gradient(${color}, ${color}), repeating-conic-gradient(#CCC 0% 25%, white 0% 50%) 50% / 16px 16px`, ...props.style }} /> ); } import {useColorSwatch} from 'react-aria'; function ColorSwatch(props) { let { colorSwatchProps, color } = useColorSwatch(props); return ( <span {...colorSwatchProps} style={{ ...colorSwatchProps.style, display: 'inline-block', width: 32, height: 32, borderRadius: 4, background: `linear-gradient(${color}, ${color}), repeating-conic-gradient(#CCC 0% 25%, white 0% 50%) 50% / 16px 16px`, ...props.style }} /> ); } import {useColorSwatch} from 'react-aria'; function ColorSwatch( props ) { let { colorSwatchProps, color } = useColorSwatch( props ); return ( <span {...colorSwatchProps} style={{ ...colorSwatchProps .style, display: 'inline-block', width: 32, height: 32, borderRadius: 4, background: `linear-gradient(${color}, ${color}), repeating-conic-gradient(#CCC 0% 25%, white 0% 50%) 50% / 16px 16px`, ...props.style }} /> ); } ### xChannel and yChannel# The color channel for each axis of a color area can be specified using the `xChannel` and `yChannel` props. An array of channel names for a color can be returned using the `color.getColorChannels` method. To get a localized channel name to use as a label, you can use the `color.getChannelName` method. #### RGB# import {parseColor} from 'react-stately'; function Example() { let [ color, setColor ] = React.useState(parseColor('rgb(100, 149, 237)')); let [ rChannel, gChannel, bChannel ] = color.getColorChannels(); return ( <> <div style={{ display: 'inline-flex', flexWrap: 'wrap' }}> <div style={{ marginRight: '2rem', marginBottom: '2rem' }}> <label id="gbr-label-id-1"> x: {color.getChannelName(gChannel, 'en-US')}, y:{' '} {color.getChannelName(bChannel, 'en-US')} </label> <ColorArea aria-labelledby="gbr-label-id-1" value={color} onChange={setColor} xChannel={gChannel} yChannel={bChannel} /> <ColorSlider channel={rChannel} value={color} onChange={setColor} /> </div> <div style={{ marginRight: '2rem', marginBottom: '2rem' }}> <label id="brg-label-id-1"> x: {color.getChannelName(bChannel, 'en-US')}, y:{' '} {color.getChannelName(rChannel, 'en-US')} </label> <ColorArea aria-labelledby="brg-label-id-1" value={color} onChange={setColor} xChannel={bChannel} yChannel={rChannel} /> <ColorSlider channel={gChannel} value={color} onChange={setColor} /> </div> <div style={{ marginRight: '2rem', marginBottom: '2rem' }}> <label id="rgb-label-id-1"> x: {color.getChannelName(rChannel, 'en-US')}, y:{' '} {color.getChannelName(gChannel, 'en-US')} </label> <ColorArea aria-labelledby="rgb-label-id-1" value={color} onChange={setColor} xChannel={rChannel} yChannel={gChannel} /> <ColorSlider channel={bChannel} value={color} onChange={setColor} /> </div> </div> <p> Current RGB color value:{' '} <ColorSwatch color={color} style={{ width: '16px', height: '16px', verticalAlign: 'text-bottom' }} />{' '} {color.toString('rgb')} </p> </> ); } import {parseColor} from 'react-stately'; function Example() { let [ color, setColor ] = React.useState(parseColor('rgb(100, 149, 237)')); let [ rChannel, gChannel, bChannel ] = color.getColorChannels(); return ( <> <div style={{ display: 'inline-flex', flexWrap: 'wrap' }} > <div style={{ marginRight: '2rem', marginBottom: '2rem' }} > <label id="gbr-label-id-1"> x: {color.getChannelName(gChannel, 'en-US')}, y: {' '} {color.getChannelName(bChannel, 'en-US')} </label> <ColorArea aria-labelledby="gbr-label-id-1" value={color} onChange={setColor} xChannel={gChannel} yChannel={bChannel} /> <ColorSlider channel={rChannel} value={color} onChange={setColor} /> </div> <div style={{ marginRight: '2rem', marginBottom: '2rem' }} > <label id="brg-label-id-1"> x: {color.getChannelName(bChannel, 'en-US')}, y: {' '} {color.getChannelName(rChannel, 'en-US')} </label> <ColorArea aria-labelledby="brg-label-id-1" value={color} onChange={setColor} xChannel={bChannel} yChannel={rChannel} /> <ColorSlider channel={gChannel} value={color} onChange={setColor} /> </div> <div style={{ marginRight: '2rem', marginBottom: '2rem' }} > <label id="rgb-label-id-1"> x: {color.getChannelName(rChannel, 'en-US')}, y: {' '} {color.getChannelName(gChannel, 'en-US')} </label> <ColorArea aria-labelledby="rgb-label-id-1" value={color} onChange={setColor} xChannel={rChannel} yChannel={gChannel} /> <ColorSlider channel={bChannel} value={color} onChange={setColor} /> </div> </div> <p> Current RGB color value:{' '} <ColorSwatch color={color} style={{ width: '16px', height: '16px', verticalAlign: 'text-bottom' }} />{' '} {color.toString('rgb')} </p> </> ); } import {parseColor} from 'react-stately'; function Example() { let [ color, setColor ] = React.useState( parseColor( 'rgb(100, 149, 237)' ) ); let [ rChannel, gChannel, bChannel ] = color .getColorChannels(); return ( <> <div style={{ display: 'inline-flex', flexWrap: 'wrap' }} > <div style={{ marginRight: '2rem', marginBottom: '2rem' }} > <label id="gbr-label-id-1"> x:{' '} {color .getChannelName( gChannel, 'en-US' )}, y:{' '} {color .getChannelName( bChannel, 'en-US' )} </label> <ColorArea aria-labelledby="gbr-label-id-1" value={color} onChange={setColor} xChannel={gChannel} yChannel={bChannel} /> <ColorSlider channel={rChannel} value={color} onChange={setColor} /> </div> <div style={{ marginRight: '2rem', marginBottom: '2rem' }} > <label id="brg-label-id-1"> x:{' '} {color .getChannelName( bChannel, 'en-US' )}, y:{' '} {color .getChannelName( rChannel, 'en-US' )} </label> <ColorArea aria-labelledby="brg-label-id-1" value={color} onChange={setColor} xChannel={bChannel} yChannel={rChannel} /> <ColorSlider channel={gChannel} value={color} onChange={setColor} /> </div> <div style={{ marginRight: '2rem', marginBottom: '2rem' }} > <label id="rgb-label-id-1"> x:{' '} {color .getChannelName( rChannel, 'en-US' )}, y:{' '} {color .getChannelName( gChannel, 'en-US' )} </label> <ColorArea aria-labelledby="rgb-label-id-1" value={color} onChange={setColor} xChannel={rChannel} yChannel={gChannel} /> <ColorSlider channel={bChannel} value={color} onChange={setColor} /> </div> </div> <p> Current RGB color value:{' '} <ColorSwatch color={color} style={{ width: '16px', height: '16px', verticalAlign: 'text-bottom' }} />{' '} {color.toString( 'rgb' )} </p> </> ); } x: Green, y: Blue Red 100 x: Blue, y: Red Green 149 x: Red, y: Green Blue 237 Current RGB color value: rgb(100, 149, 237) #### HSL# import {parseColor} from 'react-stately'; function Example() { let [ color, setColor ] = React.useState(parseColor('hsl(219, 79%, 66%)')); let [ hChannel, sChannel, lChannel ] = color.getColorChannels(); return ( <> <div style={{ display: 'inline-flex', flexWrap: 'wrap' }}> <div style={{ marginRight: '2rem', marginBottom: '2rem' }}> <label id="slh-label-id-1"> x: {color.getChannelName(sChannel, 'en-US')}, y:{' '} {color.getChannelName(lChannel, 'en-US')} </label> <ColorArea aria-labelledby="slh-label-id-1" value={color} onChange={setColor} xChannel={sChannel} yChannel={lChannel} /> <ColorSlider channel={hChannel} value={color} onChange={setColor} /> </div> <div style={{ marginRight: '2rem', marginBottom: '2rem' }}> <label id="lhs-label-id-1"> x: {color.getChannelName(hChannel, 'en-US')}, y:{' '} {color.getChannelName(lChannel, 'en-US')} </label> <ColorArea aria-labelledby="lhs-label-id-1" value={color} onChange={setColor} xChannel={hChannel} yChannel={lChannel} /> <ColorSlider channel={sChannel} value={color} onChange={setColor} /> </div> <div style={{ marginRight: '2rem', marginBottom: '2rem' }}> <label id="hsl-label-id-1"> x: {color.getChannelName(hChannel, 'en-US')}, y:{' '} {color.getChannelName(sChannel, 'en-US')} </label> <ColorArea aria-labelledby="hsl-label-id-1" value={color} onChange={setColor} xChannel={hChannel} yChannel={sChannel} /> <ColorSlider channel={lChannel} value={color} onChange={setColor} /> </div> </div> <p> Current HSL color value:{' '} <ColorSwatch color={color} aria-hidden="true" style={{ width: '16px', height: '16px', verticalAlign: 'text-bottom' }} />{' '} {color.toString('hsl')} </p> </> ); } import {parseColor} from 'react-stately'; function Example() { let [ color, setColor ] = React.useState(parseColor('hsl(219, 79%, 66%)')); let [ hChannel, sChannel, lChannel ] = color.getColorChannels(); return ( <> <div style={{ display: 'inline-flex', flexWrap: 'wrap' }} > <div style={{ marginRight: '2rem', marginBottom: '2rem' }} > <label id="slh-label-id-1"> x: {color.getChannelName(sChannel, 'en-US')}, y: {' '} {color.getChannelName(lChannel, 'en-US')} </label> <ColorArea aria-labelledby="slh-label-id-1" value={color} onChange={setColor} xChannel={sChannel} yChannel={lChannel} /> <ColorSlider channel={hChannel} value={color} onChange={setColor} /> </div> <div style={{ marginRight: '2rem', marginBottom: '2rem' }} > <label id="lhs-label-id-1"> x: {color.getChannelName(hChannel, 'en-US')}, y: {' '} {color.getChannelName(lChannel, 'en-US')} </label> <ColorArea aria-labelledby="lhs-label-id-1" value={color} onChange={setColor} xChannel={hChannel} yChannel={lChannel} /> <ColorSlider channel={sChannel} value={color} onChange={setColor} /> </div> <div style={{ marginRight: '2rem', marginBottom: '2rem' }} > <label id="hsl-label-id-1"> x: {color.getChannelName(hChannel, 'en-US')}, y: {' '} {color.getChannelName(sChannel, 'en-US')} </label> <ColorArea aria-labelledby="hsl-label-id-1" value={color} onChange={setColor} xChannel={hChannel} yChannel={sChannel} /> <ColorSlider channel={lChannel} value={color} onChange={setColor} /> </div> </div> <p> Current HSL color value:{' '} <ColorSwatch color={color} aria-hidden="true" style={{ width: '16px', height: '16px', verticalAlign: 'text-bottom' }} />{' '} {color.toString('hsl')} </p> </> ); } import {parseColor} from 'react-stately'; function Example() { let [ color, setColor ] = React.useState( parseColor( 'hsl(219, 79%, 66%)' ) ); let [ hChannel, sChannel, lChannel ] = color .getColorChannels(); return ( <> <div style={{ display: 'inline-flex', flexWrap: 'wrap' }} > <div style={{ marginRight: '2rem', marginBottom: '2rem' }} > <label id="slh-label-id-1"> x:{' '} {color .getChannelName( sChannel, 'en-US' )}, y:{' '} {color .getChannelName( lChannel, 'en-US' )} </label> <ColorArea aria-labelledby="slh-label-id-1" value={color} onChange={setColor} xChannel={sChannel} yChannel={lChannel} /> <ColorSlider channel={hChannel} value={color} onChange={setColor} /> </div> <div style={{ marginRight: '2rem', marginBottom: '2rem' }} > <label id="lhs-label-id-1"> x:{' '} {color .getChannelName( hChannel, 'en-US' )}, y:{' '} {color .getChannelName( lChannel, 'en-US' )} </label> <ColorArea aria-labelledby="lhs-label-id-1" value={color} onChange={setColor} xChannel={hChannel} yChannel={lChannel} /> <ColorSlider channel={sChannel} value={color} onChange={setColor} /> </div> <div style={{ marginRight: '2rem', marginBottom: '2rem' }} > <label id="hsl-label-id-1"> x:{' '} {color .getChannelName( hChannel, 'en-US' )}, y:{' '} {color .getChannelName( sChannel, 'en-US' )} </label> <ColorArea aria-labelledby="hsl-label-id-1" value={color} onChange={setColor} xChannel={hChannel} yChannel={sChannel} /> <ColorSlider channel={lChannel} value={color} onChange={setColor} /> </div> </div> <p> Current HSL color value:{' '} <ColorSwatch color={color} aria-hidden="true" style={{ width: '16px', height: '16px', verticalAlign: 'text-bottom' }} />{' '} {color.toString( 'hsl' )} </p> </> ); } x: Saturation, y: Lightness Hue 219° x: Hue, y: Lightness Saturation 79% x: Hue, y: Saturation Lightness 66% Current HSL color value: hsl(219, 79%, 66%) #### HSB# import {parseColor} from 'react-stately'; function Example() { let [ color, setColor ] = React.useState(parseColor('hsb(219, 58%, 93%)')); let [ hChannel, sChannel, bChannel ] = color.getColorChannels(); return ( <> <div style={{ display: 'inline-flex', flexWrap: 'wrap' }}> <div style={{ marginRight: '2rem', marginBottom: '2rem' }}> <label id="sbh-label-id-1"> x: {color.getChannelName(sChannel, 'en-US')}, y:{' '} {color.getChannelName(bChannel, 'en-US')} </label> <ColorArea aria-labelledby="sbh-label-id-1" value={color} onChange={setColor} xChannel={sChannel} yChannel={bChannel} /> <ColorSlider channel={hChannel} value={color} onChange={setColor} /> </div> <div style={{ marginRight: '2rem', marginBottom: '2rem' }}> <label id="bhs-label-id-1"> x: {color.getChannelName(hChannel, 'en-US')}, y:{' '} {color.getChannelName(bChannel, 'en-US')} </label> <ColorArea aria-labelledby="bhs-label-id-1" value={color} onChange={setColor} xChannel={hChannel} yChannel={bChannel} /> <ColorSlider channel={sChannel} value={color} onChange={setColor} /> </div> <div style={{ marginRight: '2rem', marginBottom: '2rem' }}> <label id="hsb-label-id-2"> x: {color.getChannelName(hChannel, 'en-US')}, y:{' '} {color.getChannelName(sChannel, 'en-US')} </label> <ColorArea aria-labelledby="hsb-label-id-2" value={color} onChange={setColor} xChannel={hChannel} yChannel={sChannel} /> <ColorSlider channel={bChannel} value={color} onChange={setColor} /> </div> </div> <p> Current HSB color value:{' '} <ColorSwatch color={color} style={{ width: '16px', height: '16px', verticalAlign: 'text-bottom' }} />{' '} {color.toString('hsb')} </p> </> ); } import {parseColor} from 'react-stately'; function Example() { let [ color, setColor ] = React.useState(parseColor('hsb(219, 58%, 93%)')); let [ hChannel, sChannel, bChannel ] = color.getColorChannels(); return ( <> <div style={{ display: 'inline-flex', flexWrap: 'wrap' }} > <div style={{ marginRight: '2rem', marginBottom: '2rem' }} > <label id="sbh-label-id-1"> x: {color.getChannelName(sChannel, 'en-US')}, y: {' '} {color.getChannelName(bChannel, 'en-US')} </label> <ColorArea aria-labelledby="sbh-label-id-1" value={color} onChange={setColor} xChannel={sChannel} yChannel={bChannel} /> <ColorSlider channel={hChannel} value={color} onChange={setColor} /> </div> <div style={{ marginRight: '2rem', marginBottom: '2rem' }} > <label id="bhs-label-id-1"> x: {color.getChannelName(hChannel, 'en-US')}, y: {' '} {color.getChannelName(bChannel, 'en-US')} </label> <ColorArea aria-labelledby="bhs-label-id-1" value={color} onChange={setColor} xChannel={hChannel} yChannel={bChannel} /> <ColorSlider channel={sChannel} value={color} onChange={setColor} /> </div> <div style={{ marginRight: '2rem', marginBottom: '2rem' }} > <label id="hsb-label-id-2"> x: {color.getChannelName(hChannel, 'en-US')}, y: {' '} {color.getChannelName(sChannel, 'en-US')} </label> <ColorArea aria-labelledby="hsb-label-id-2" value={color} onChange={setColor} xChannel={hChannel} yChannel={sChannel} /> <ColorSlider channel={bChannel} value={color} onChange={setColor} /> </div> </div> <p> Current HSB color value:{' '} <ColorSwatch color={color} style={{ width: '16px', height: '16px', verticalAlign: 'text-bottom' }} />{' '} {color.toString('hsb')} </p> </> ); } import {parseColor} from 'react-stately'; function Example() { let [ color, setColor ] = React.useState( parseColor( 'hsb(219, 58%, 93%)' ) ); let [ hChannel, sChannel, bChannel ] = color .getColorChannels(); return ( <> <div style={{ display: 'inline-flex', flexWrap: 'wrap' }} > <div style={{ marginRight: '2rem', marginBottom: '2rem' }} > <label id="sbh-label-id-1"> x:{' '} {color .getChannelName( sChannel, 'en-US' )}, y:{' '} {color .getChannelName( bChannel, 'en-US' )} </label> <ColorArea aria-labelledby="sbh-label-id-1" value={color} onChange={setColor} xChannel={sChannel} yChannel={bChannel} /> <ColorSlider channel={hChannel} value={color} onChange={setColor} /> </div> <div style={{ marginRight: '2rem', marginBottom: '2rem' }} > <label id="bhs-label-id-1"> x:{' '} {color .getChannelName( hChannel, 'en-US' )}, y:{' '} {color .getChannelName( bChannel, 'en-US' )} </label> <ColorArea aria-labelledby="bhs-label-id-1" value={color} onChange={setColor} xChannel={hChannel} yChannel={bChannel} /> <ColorSlider channel={sChannel} value={color} onChange={setColor} /> </div> <div style={{ marginRight: '2rem', marginBottom: '2rem' }} > <label id="hsb-label-id-2"> x:{' '} {color .getChannelName( hChannel, 'en-US' )}, y:{' '} {color .getChannelName( sChannel, 'en-US' )} </label> <ColorArea aria-labelledby="hsb-label-id-2" value={color} onChange={setColor} xChannel={hChannel} yChannel={sChannel} /> <ColorSlider channel={bChannel} value={color} onChange={setColor} /> </div> </div> <p> Current HSB color value:{' '} <ColorSwatch color={color} style={{ width: '16px', height: '16px', verticalAlign: 'text-bottom' }} />{' '} {color.toString( 'hsb' )} </p> </> ); } x: Saturation, y: Brightness Hue 219° x: Hue, y: Brightness Saturation 58% x: Hue, y: Saturation Brightness 93% Current HSB color value: hsb(219, 58%, 93%) ### Disabled# A color area can be disabled using the `isDisabled` prop. This prevents the thumb from being focused or dragged. It's up to you to style your color area to appear disabled accordingly. <ColorArea defaultValue="hsl(0, 100%, 50%)" xChannel="saturation" yChannel="lightness" isDisabled /> <ColorArea defaultValue="hsl(0, 100%, 50%)" xChannel="saturation" yChannel="lightness" isDisabled /> <ColorArea defaultValue="hsl(0, 100%, 50%)" xChannel="saturation" yChannel="lightness" isDisabled /> ### HTML forms# ColorArea supports the `xName` and `yName` props for integration with HTML forms. The values will be submitted as numbers between the minimum and maximum value for the corresponding channel in the X and Y direction. <ColorArea xName="red" yName="green" /> <ColorArea xName="red" yName="green" /> <ColorArea xName="red" yName="green" /> ## Internationalization# * * * ### Labeling# By default, `useColorArea` provides an `aria-label` for the localized string "Color Picker", which labels the visually hidden `<input>` elements for the two color channels, or on mobile devices, the group containing them. If you wish to override this with a more specific label, an `aria-label` or `aria-labelledby` prop may be passed to further to identify the element to assistive technologies. For example, for a color area that adjusts a text color you might pass the `aria-label` prop, "Text color", which `useColorArea` will return as the `aria-label` prop "Text color, Color picker". When a custom `aria-label` is provided, it should be localized accordingly. ### Role description# In order to communicate to a screen reader user that the color area adjusts in two dimensions, `useColorArea` provides an `aria-roledescription`, using the localized string "2D Slider", on each of the visually hidden `<input>` elements. ### Value formatting# The `aria-valuetext` of each `<input>` element is formatted according to the user's locale automatically. It includes the localized color channel name and current value for each channel, with the channel name and value that the `<input>` element controls coming before the channel name and value for the adjacent `<input>` element. For example, for an RGB color area where the `xChannel` is "blue", the `yChannel` is "green", when the current selected color is yellow (`rgb(255, 255, 0)`), the `<input>` representing the blue channel will have `aria-valuetext` to announce as "Blue: 0, Green: 255", and the `<input>` representing the green channel will have `aria-valuetext` to announce as "Green: 255, Blue: 0". ### RTL# In right-to-left languages, color areas should be mirrored. Orientation of the gradient background, positioning of the thumb, and dragging behavior is automatically mirrored by `useColorArea`. --- ## Page: https://react-spectrum.adobe.com/react-aria/useColorField.html # useColorField Provides the behavior and accessibility implementation for a color field component. Color fields allow users to enter and adjust a hex color value. <table><tbody><tr><th>install</th><td><code>yarn add react-aria</code></td></tr><tr><th>version</th><td>3.41.1</td></tr><tr><th>usage</th><td><code><span>import</span> {useColorField} <span>from</span> <span>'react-aria'</span></code></td></tr></tbody></table> View ARIA pattern W3C View repository GitHub View package NPM ## API# * * * `useColorField( props: AriaColorFieldProps , state: ColorFieldState , ref: RefObject<HTMLInputElement | | null> ): ColorFieldAria ` ## Features# * * * The <input type="color"> HTML element can be used to build a color picker, however it is very inconsistent across browsers and operating systems and consists of a complete color picker rather than a single field for editing a hex value. `useColorField` helps achieve accessible color fields that can be styled as needed. * Support for parsing and formatting a hex color value * Validates keyboard entry as the user types so that only valid hex characters are accepted * Supports using the arrow keys to increment and decrement the value * Exposed to assistive technology as a `textbox` via ARIA * Visual and ARIA labeling support * Follows the spinbutton ARIA pattern * Works around bugs in VoiceOver with the spinbutton role * Uses an ARIA live region to ensure that value changes are announced ## Anatomy# * * * A color field consists of an input element and a label. `useColorField` automatically manages the relationship between the two elements using the `for` attribute on the `<label>` element and the `aria-labelledby` attribute on the `<input>` element. `useColorField` returns two sets of props that you should spread onto the appropriate element: | Name | Type | Description | | --- | --- | --- | | `labelProps` | `LabelHTMLAttributes<HTMLLabelElement>` | Props for the label element. | | `inputProps` | `InputHTMLAttributes<HTMLInputElement>` | Props for the input element. | | `descriptionProps` | `DOMAttributes` | Props for the text field's description element, if any. | | `errorMessageProps` | `DOMAttributes` | Props for the text field's error message element, if any. | | `isInvalid` | `boolean` | Whether the input value is invalid. | | `validationErrors` | `string[]` | The current error messages for the input if it is invalid, otherwise an empty array. | | `validationDetails` | `ValidityState` | The native validation details for the input. | State is managed by the `useColorFieldState` hook from `@react-stately/color`. The state object should be passed as an option to `useColorField` If there is no visual label, an `aria-label` or `aria-labelledby` prop must be passed instead to identify the element to screen readers. ## Example# * * * import {useColorFieldState} from 'react-stately'; import {useColorField} from 'react-aria'; function ColorField(props) { let state = useColorFieldState(props); let inputRef = React.useRef(null); let { labelProps, inputProps } = useColorField(props, state, inputRef); return ( <div style={{ display: 'inline-flex', flexDirection: 'column' }}> <label {...labelProps}>{props.label}</label> <input {...inputProps} ref={inputRef} /> </div> ); } <ColorField label="Color" /> import {useColorFieldState} from 'react-stately'; import {useColorField} from 'react-aria'; function ColorField(props) { let state = useColorFieldState(props); let inputRef = React.useRef(null); let { labelProps, inputProps } = useColorField(props, state, inputRef); return ( <div style={{ display: 'inline-flex', flexDirection: 'column' }} > <label {...labelProps}>{props.label}</label> <input {...inputProps} ref={inputRef} /> </div> ); } <ColorField label="Color" /> import {useColorFieldState} from 'react-stately'; import {useColorField} from 'react-aria'; function ColorField( props ) { let state = useColorFieldState( props ); let inputRef = React .useRef(null); let { labelProps, inputProps } = useColorField( props, state, inputRef ); return ( <div style={{ display: 'inline-flex', flexDirection: 'column' }} > <label {...labelProps} > {props.label} </label> <input {...inputProps} ref={inputRef} /> </div> ); } <ColorField label="Color" /> Color ## Usage# * * * The following examples show how to use the `ColorField` component created in the above example. ### Uncontrolled# By default, `ColorField` is uncontrolled. You can set a default value using the `defaultValue` prop. <ColorField aria-label="Color" defaultValue="#7f007f" /> <ColorField aria-label="Color" defaultValue="#7f007f" /> <ColorField aria-label="Color" defaultValue="#7f007f" /> ### Controlled# A `ColorField` can be made controlled. The `parseColor` function is used to parse the initial color from a hex string, stored in state. The `value` and `onChange` props are used to update the value in state when the edits the value. import {parseColor} from 'react-stately'; function Example() { let [color, setColor] = React.useState(parseColor('#7f007f')); return ( <> <ColorField aria-label="Color" value={color} onChange={setColor} /> <p>Current color value: {color.toString('hex')}</p> </> ); } import {parseColor} from 'react-stately'; function Example() { let [color, setColor] = React.useState( parseColor('#7f007f') ); return ( <> <ColorField aria-label="Color" value={color} onChange={setColor} /> <p>Current color value: {color.toString('hex')}</p> </> ); } import {parseColor} from 'react-stately'; function Example() { let [color, setColor] = React.useState( parseColor( '#7f007f' ) ); return ( <> <ColorField aria-label="Color" value={color} onChange={setColor} /> <p> Current color value:{' '} {color.toString( 'hex' )} </p> </> ); } Current color value: #7F007F ### Disabled and read only# A `ColorField` can be disabled using the `isDisabled` prop, and made read only using the `isReadOnly` prop. The difference is that read only color fields are focusable but disabled color fields are not. <ColorField aria-label="Color" defaultValue="#7f007f" isDisabled /> <ColorField aria-label="Color" defaultValue="#7f007f" isReadOnly /> <ColorField aria-label="Color" defaultValue="#7f007f" isDisabled /> <ColorField aria-label="Color" defaultValue="#7f007f" isReadOnly /> <ColorField aria-label="Color" defaultValue="#7f007f" isDisabled /> <ColorField aria-label="Color" defaultValue="#7f007f" isReadOnly /> ### HTML forms# ColorField supports the `name` prop for integration with HTML forms. The value will be submitted to the server as a hex color string. <ColorField label="Color" name="color" /> <ColorField label="Color" name="color" /> <ColorField label="Color" name="color" /> Color ## Internationalization# * * * ### RTL# In right-to-left languages, color fields should be mirrored. The label should be right aligned, along with the text in the input. Ensure that your CSS accounts for this. --- ## Page: https://react-spectrum.adobe.com/react-aria/useColorSlider.html # useColorSlider Provides the behavior and accessibility implementation for a color slider component. Color sliders allow users to adjust an individual channel of a color value. <table><tbody><tr><th>install</th><td><code>yarn add react-aria</code></td></tr><tr><th>version</th><td>3.41.1</td></tr><tr><th>usage</th><td><code><span>import</span> {useColorSlider} <span>from</span> <span>'react-aria'</span></code></td></tr></tbody></table> View ARIA pattern W3C View repository GitHub View package NPM ## API# * * * `useColorSlider( (props: AriaColorSliderOptions , , state: ColorSliderState )): ColorSliderAria ` ## Features# * * * The <input type="color"> HTML element can be used to build a color picker, however it is very inconsistent across browsers and operating systems and consists of a complete color picker rather than a single color channel slider. `useColorSlider` helps achieve accessible and touch-friendly color sliders that can be styled as needed. * Support for adjusting a single channel of RGBA, HSLA, and HSBA colors * Support for mouse, touch, and keyboard via the useMove hook * Multi-touch support for dragging multiple sliders at once * Pressing on the track moves the thumb to that position * Supports using the arrow keys, as well as page up/down, home, and end keys * Support for both horizontal and vertical orientations * Support for disabling the color slider * Prevents text selection while dragging * Exposed to assistive technology as a `slider` element via ARIA * Uses a hidden native input element to support touch screen readers * Automatic ARIA labeling using localized channel names by default * Support for visually labeling the slider * Support for displaying the current value using an `<output>` element * Internationalized number formatting based on the color channel type * Support for mirroring in RTL locales ## Anatomy# * * * A color slider consists of a track element and a thumb that the user can drag to change a single channel of a color value. It may also include optional label and `<output>` elements to display the color channel name and current numeric value, respectively. A visually hidden `<input>` element is used to represent the value to assistive technologies. `useColorSlider` returns props that you should spread onto the appropriate elements: | Name | Type | Description | | --- | --- | --- | | `labelProps` | `DOMAttributes` | Props for the label element. | | `trackProps` | `DOMAttributes` | Props for the track element. | | `thumbProps` | `DOMAttributes` | Props for the thumb element. | | `inputProps` | `InputHTMLAttributes<HTMLInputElement>` | Props for the visually hidden range input element. | | `outputProps` | `DOMAttributes` | Props for the output element, displaying the value of the color slider. | State is managed by the `useColorSliderState` hook from `@react-stately/color`. The state object should be passed as an option to `useColorSlider` By default, `useColorSlider` provides an `aria-label` for the localized color channel name. If you wish to display a visual label, or override this with a more specific label, a `label`, `aria-label` or `aria-labelledby` prop may be passed instead. ## Example# * * * This example shows how to build a horizontal color slider. It also includes a label which can be clicked to focus the thumb. Styling for the track background and positioning of the thumb are provided by `useColorSlider` in the returned `style` prop for each element. If no `label` prop is given, it uses the `Color` object to get a localized string for the channel name using the `getChannelName` method. In addition, an `<output>` element is used to display the current channel value as text. This is formatted using the Color object's `formatChannelValue` method, which formats the value according to the channel type and locale settings. The visually hidden `<input>` element inside the thumb is used to represent the color slider to assistive technology. The thumb also uses the useFocusRing hook to grow in size when it is keyboard focused (try tabbing to it). import {useColorSliderState} from 'react-stately'; import {useColorSlider, useFocusRing, useLocale} from 'react-aria'; const TRACK_THICKNESS = 28; const THUMB_SIZE = 20; function ColorSlider(props) { let { isDisabled } = props; let { locale } = useLocale(); let state = useColorSliderState({ ...props, locale }); let trackRef = React.useRef(null); let inputRef = React.useRef(null); // Default label to the channel name in the current locale let label = props.label || state.value.getChannelName(props.channel, locale); let { trackProps, thumbProps, inputProps, labelProps, outputProps } = useColorSlider({ ...props, label, trackRef, inputRef }, state); let { focusProps, isFocusVisible } = useFocusRing(); return ( <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', width: 300 }} > {/* Create a flex container for the label and output element. */} <div style={{ display: 'flex', alignSelf: 'stretch' }}> <label {...labelProps}>{label}</label> <output {...outputProps} style={{ flex: '1 0 auto', textAlign: 'end' }}> {state.value.formatChannelValue(props.channel, locale)} </output> </div> {/* The track element holds the visible track line and the thumb. */} <div {...trackProps} ref={trackRef} style={{ ...trackProps.style, height: TRACK_THICKNESS, width: '100%', borderRadius: 4, background: isDisabled ? 'rgb(142, 142, 142)' : trackProps.style.background }} > <div {...thumbProps} style={{ ...thumbProps.style, top: TRACK_THICKNESS / 2, background: isDisabled ? 'rgb(142, 142, 142)' : state.getDisplayColor().toString('css'), border: `2px solid ${isDisabled ? 'rgb(142, 142, 142)' : 'white'}`, boxShadow: '0 0 0 1px black, inset 0 0 0 1px black', width: isFocusVisible ? TRACK_THICKNESS + 4 : THUMB_SIZE, height: isFocusVisible ? TRACK_THICKNESS + 4 : THUMB_SIZE, borderRadius: '50%', boxSizing: 'border-box' }} > <input ref={inputRef} {...inputProps} {...focusProps} /> </div> </div> </div> ); } <ColorSlider channel="hue" defaultValue="hsl(0, 100%, 50%)" /> import {useColorSliderState} from 'react-stately'; import { useColorSlider, useFocusRing, useLocale } from 'react-aria'; const TRACK_THICKNESS = 28; const THUMB_SIZE = 20; function ColorSlider(props) { let { isDisabled } = props; let { locale } = useLocale(); let state = useColorSliderState({ ...props, locale }); let trackRef = React.useRef(null); let inputRef = React.useRef(null); // Default label to the channel name in the current locale let label = props.label || state.value.getChannelName(props.channel, locale); let { trackProps, thumbProps, inputProps, labelProps, outputProps } = useColorSlider({ ...props, label, trackRef, inputRef }, state); let { focusProps, isFocusVisible } = useFocusRing(); return ( <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', width: 300 }} > {/* Create a flex container for the label and output element. */} <div style={{ display: 'flex', alignSelf: 'stretch' }} > <label {...labelProps}>{label}</label> <output {...outputProps} style={{ flex: '1 0 auto', textAlign: 'end' }} > {state.value.formatChannelValue( props.channel, locale )} </output> </div> {/* The track element holds the visible track line and the thumb. */} <div {...trackProps} ref={trackRef} style={{ ...trackProps.style, height: TRACK_THICKNESS, width: '100%', borderRadius: 4, background: isDisabled ? 'rgb(142, 142, 142)' : trackProps.style.background }} > <div {...thumbProps} style={{ ...thumbProps.style, top: TRACK_THICKNESS / 2, background: isDisabled ? 'rgb(142, 142, 142)' : state.getDisplayColor().toString('css'), border: `2px solid ${ isDisabled ? 'rgb(142, 142, 142)' : 'white' }`, boxShadow: '0 0 0 1px black, inset 0 0 0 1px black', width: isFocusVisible ? TRACK_THICKNESS + 4 : THUMB_SIZE, height: isFocusVisible ? TRACK_THICKNESS + 4 : THUMB_SIZE, borderRadius: '50%', boxSizing: 'border-box' }} > <input ref={inputRef} {...inputProps} {...focusProps} /> </div> </div> </div> ); } <ColorSlider channel="hue" defaultValue="hsl(0, 100%, 50%)" /> import {useColorSliderState} from 'react-stately'; import { useColorSlider, useFocusRing, useLocale } from 'react-aria'; const TRACK_THICKNESS = 28; const THUMB_SIZE = 20; function ColorSlider( props ) { let { isDisabled } = props; let { locale } = useLocale(); let state = useColorSliderState({ ...props, locale }); let trackRef = React .useRef(null); let inputRef = React .useRef(null); // Default label to the channel name in the current locale let label = props.label || state.value .getChannelName( props.channel, locale ); let { trackProps, thumbProps, inputProps, labelProps, outputProps } = useColorSlider({ ...props, label, trackRef, inputRef }, state); let { focusProps, isFocusVisible } = useFocusRing(); return ( <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', width: 300 }} > {/* Create a flex container for the label and output element. */} <div style={{ display: 'flex', alignSelf: 'stretch' }} > <label {...labelProps} > {label} </label> <output {...outputProps} style={{ flex: '1 0 auto', textAlign: 'end' }} > {state.value .formatChannelValue( props .channel, locale )} </output> </div> {/* The track element holds the visible track line and the thumb. */} <div {...trackProps} ref={trackRef} style={{ ...trackProps .style, height: TRACK_THICKNESS, width: '100%', borderRadius: 4, background: isDisabled ? 'rgb(142, 142, 142)' : trackProps .style .background }} > <div {...thumbProps} style={{ ...thumbProps .style, top: TRACK_THICKNESS / 2, background: isDisabled ? 'rgb(142, 142, 142)' : state .getDisplayColor() .toString( 'css' ), border: `2px solid ${ isDisabled ? 'rgb(142, 142, 142)' : 'white' }`, boxShadow: '0 0 0 1px black, inset 0 0 0 1px black', width: isFocusVisible ? TRACK_THICKNESS + 4 : THUMB_SIZE, height: isFocusVisible ? TRACK_THICKNESS + 4 : THUMB_SIZE, borderRadius: '50%', boxSizing: 'border-box' }} > <input ref={inputRef} {...inputProps} {...focusProps} /> </div> </div> </div> ); } <ColorSlider channel="hue" defaultValue="hsl(0, 100%, 50%)" /> Hue 0° ### Vertical# This example shows how to build a vertical color slider. The main difference from horizontal color sliders is the addition of the `orientation: 'vertical'` option to `useColorSlider`. This automatically adjusts the internal positioning and dragging logic. Additionally, this example does not have a visible label or `<output>` element. This can be done by simply not using the returned `labelProps` and `outputProps`. The color slider will have a default `aria-label` using the localized channel name, which can be overridden by passing an `aria-label` prop to `useColorSlider`. function ColorSlider(props) { let { locale } = useLocale(); let state = useColorSliderState({ ...props, locale }); let trackRef = React.useRef(null); let inputRef = React.useRef(null); let { trackProps, thumbProps, inputProps } = useColorSlider({ ...props, orientation: 'vertical', trackRef, inputRef }, state); let { focusProps, isFocusVisible } = useFocusRing(); return ( <div style={{ height: 200 }} > <div {...trackProps} ref={trackRef} style={{ ...trackProps.style, width: TRACK_THICKNESS, height: '100%', borderRadius: 4 }} > <div {...thumbProps} style={{ ...thumbProps.style, left: TRACK_THICKNESS / 2, border: '2px solid white', boxShadow: '0 0 0 1px black, inset 0 0 0 1px black', width: isFocusVisible ? TRACK_THICKNESS + 4 : THUMB_SIZE, height: isFocusVisible ? TRACK_THICKNESS + 4 : THUMB_SIZE, borderRadius: '50%', boxSizing: 'border-box', background: state.getDisplayColor().toString('css') }} > <input ref={inputRef} {...inputProps} {...focusProps} /> </div> </div> </div> ); } <ColorSlider channel="hue" defaultValue="hsl(0, 100%, 50%)" /> function ColorSlider(props) { let { locale } = useLocale(); let state = useColorSliderState({ ...props, locale }); let trackRef = React.useRef(null); let inputRef = React.useRef(null); let { trackProps, thumbProps, inputProps } = useColorSlider({ ...props, orientation: 'vertical', trackRef, inputRef }, state); let { focusProps, isFocusVisible } = useFocusRing(); return ( <div style={{ height: 200 }} > <div {...trackProps} ref={trackRef} style={{ ...trackProps.style, width: TRACK_THICKNESS, height: '100%', borderRadius: 4 }} > <div {...thumbProps} style={{ ...thumbProps.style, left: TRACK_THICKNESS / 2, border: '2px solid white', boxShadow: '0 0 0 1px black, inset 0 0 0 1px black', width: isFocusVisible ? TRACK_THICKNESS + 4 : THUMB_SIZE, height: isFocusVisible ? TRACK_THICKNESS + 4 : THUMB_SIZE, borderRadius: '50%', boxSizing: 'border-box', background: state.getDisplayColor().toString( 'css' ) }} > <input ref={inputRef} {...inputProps} {...focusProps} /> </div> </div> </div> ); } <ColorSlider channel="hue" defaultValue="hsl(0, 100%, 50%)" /> function ColorSlider( props ) { let { locale } = useLocale(); let state = useColorSliderState({ ...props, locale }); let trackRef = React .useRef(null); let inputRef = React .useRef(null); let { trackProps, thumbProps, inputProps } = useColorSlider({ ...props, orientation: 'vertical', trackRef, inputRef }, state); let { focusProps, isFocusVisible } = useFocusRing(); return ( <div style={{ height: 200 }} > <div {...trackProps} ref={trackRef} style={{ ...trackProps .style, width: TRACK_THICKNESS, height: '100%', borderRadius: 4 }} > <div {...thumbProps} style={{ ...thumbProps .style, left: TRACK_THICKNESS / 2, border: '2px solid white', boxShadow: '0 0 0 1px black, inset 0 0 0 1px black', width: isFocusVisible ? TRACK_THICKNESS + 4 : THUMB_SIZE, height: isFocusVisible ? TRACK_THICKNESS + 4 : THUMB_SIZE, borderRadius: '50%', boxSizing: 'border-box', background: state .getDisplayColor() .toString( 'css' ) }} > <input ref={inputRef} {...inputProps} {...focusProps} /> </div> </div> </div> ); } <ColorSlider channel="hue" defaultValue="hsl(0, 100%, 50%)" /> ## Usage# * * * The following examples show how to use the `ColorSlider` component created in the above example. ### RGBA# This example shows how you could build an RGBA color picker using four color sliders bound to the same color value in state. The `parseColor` function is used to parse the initial color from a hex value, stored in state. The `value` and `onChange` props of `ColorSlider` are used to make the sliders controlled, so that they all update when the color is modified. import {parseColor} from 'react-stately'; function Example() { let [color, setColor] = React.useState(parseColor('#7f007f')); return ( <> <ColorSlider channel="red" value={color} onChange={setColor} /> <ColorSlider channel="green" value={color} onChange={setColor} /> <ColorSlider channel="blue" value={color} onChange={setColor} /> <ColorSlider channel="alpha" value={color} onChange={setColor} /> </> ); } import {parseColor} from 'react-stately'; function Example() { let [color, setColor] = React.useState( parseColor('#7f007f') ); return ( <> <ColorSlider channel="red" value={color} onChange={setColor} /> <ColorSlider channel="green" value={color} onChange={setColor} /> <ColorSlider channel="blue" value={color} onChange={setColor} /> <ColorSlider channel="alpha" value={color} onChange={setColor} /> </> ); } import {parseColor} from 'react-stately'; function Example() { let [color, setColor] = React.useState( parseColor( '#7f007f' ) ); return ( <> <ColorSlider channel="red" value={color} onChange={setColor} /> <ColorSlider channel="green" value={color} onChange={setColor} /> <ColorSlider channel="blue" value={color} onChange={setColor} /> <ColorSlider channel="alpha" value={color} onChange={setColor} /> </> ); } Red 127 Green 0 Blue 127 Alpha 100% ### HSLA# This example shows how to build a similar color picker to the one above, using HSLA colors instead. function Example() { let [color, setColor] = React.useState(parseColor('hsl(0, 100%, 50%)')); return ( <> <ColorSlider channel="hue" value={color} onChange={setColor} /> <ColorSlider channel="saturation" value={color} onChange={setColor} /> <ColorSlider channel="lightness" value={color} onChange={setColor} /> <ColorSlider channel="alpha" value={color} onChange={setColor} /> </> ); } function Example() { let [color, setColor] = React.useState( parseColor('hsl(0, 100%, 50%)') ); return ( <> <ColorSlider channel="hue" value={color} onChange={setColor} /> <ColorSlider channel="saturation" value={color} onChange={setColor} /> <ColorSlider channel="lightness" value={color} onChange={setColor} /> <ColorSlider channel="alpha" value={color} onChange={setColor} /> </> ); } function Example() { let [color, setColor] = React.useState( parseColor( 'hsl(0, 100%, 50%)' ) ); return ( <> <ColorSlider channel="hue" value={color} onChange={setColor} /> <ColorSlider channel="saturation" value={color} onChange={setColor} /> <ColorSlider channel="lightness" value={color} onChange={setColor} /> <ColorSlider channel="alpha" value={color} onChange={setColor} /> </> ); } Hue 0° Saturation 100% Lightness 50% Alpha 100% ### HSBA# This example shows how to build an HSBA color picker. function Example() { let [color, setColor] = React.useState(parseColor('hsb(0, 100%, 50%)')); return ( <> <ColorSlider channel="hue" value={color} onChange={setColor} /> <ColorSlider channel="saturation" value={color} onChange={setColor} /> <ColorSlider channel="brightness" value={color} onChange={setColor} /> <ColorSlider channel="alpha" value={color} onChange={setColor} /> </> ); } function Example() { let [color, setColor] = React.useState( parseColor('hsb(0, 100%, 50%)') ); return ( <> <ColorSlider channel="hue" value={color} onChange={setColor} /> <ColorSlider channel="saturation" value={color} onChange={setColor} /> <ColorSlider channel="brightness" value={color} onChange={setColor} /> <ColorSlider channel="alpha" value={color} onChange={setColor} /> </> ); } function Example() { let [color, setColor] = React.useState( parseColor( 'hsb(0, 100%, 50%)' ) ); return ( <> <ColorSlider channel="hue" value={color} onChange={setColor} /> <ColorSlider channel="saturation" value={color} onChange={setColor} /> <ColorSlider channel="brightness" value={color} onChange={setColor} /> <ColorSlider channel="alpha" value={color} onChange={setColor} /> </> ); } Hue 0° Saturation 100% Brightness 50% Alpha 100% ### onChangeEnd# The `onChangeEnd` prop can be used to handle when a user stops dragging a color slider, whereas the `onChange` prop is called as the user drags. function Example() { let [color, setColor] = React.useState(parseColor('#7f007f')); return ( <> <ColorSlider channel="red" defaultValue={color} onChangeEnd={setColor} /> <p>Current color value: {color.toString('hex')}</p> </> ); } function Example() { let [color, setColor] = React.useState( parseColor('#7f007f') ); return ( <> <ColorSlider channel="red" defaultValue={color} onChangeEnd={setColor} /> <p>Current color value: {color.toString('hex')}</p> </> ); } function Example() { let [color, setColor] = React.useState( parseColor( '#7f007f' ) ); return ( <> <ColorSlider channel="red" defaultValue={color} onChangeEnd={setColor} /> <p> Current color value:{' '} {color.toString( 'hex' )} </p> </> ); } Red 127 Current color value: #7F007F ### Disabled# A `ColorSlider` can be disabled using the `isDisabled` prop. This prevents the thumb from being focused or dragged. It's up to you to style your color slider to appear disabled accordingly. <ColorSlider channel="red" defaultValue="#7f007f" isDisabled /> <ColorSlider channel="red" defaultValue="#7f007f" isDisabled /> <ColorSlider channel="red" defaultValue="#7f007f" isDisabled /> Red 127 ### HTML forms# ColorSlider supports the `name` prop for integration with HTML forms. The value will be submitted as a number between the minimum and maximum value for the displayed channel. <ColorSlider defaultValue="#7f0000" channel="red" name="red" /> <ColorSlider defaultValue="#7f0000" channel="red" name="red" /> <ColorSlider defaultValue="#7f0000" channel="red" name="red" /> Red 127 ## Internationalization# * * * ### Labeling# By default, a localized string for the channel name is used as the `aria-label` for the `ColorSlider`. When a custom `aria-label` or visual `label` is provided, it should be localized accordingly. To get a localized channel name to use as the visual label, you can use the `color.getChannelName` method. ### Value formatting# The `aria-valuetext` of the `<input>` element is formatted according to the user's locale automatically. If you wish to display this value visually in the `<output>` element, you can use the `color.formatChannelValue` method. ### RTL# In right-to-left languages, color sliders should be mirrored. The label should be right aligned, and the value should be left aligned. Ensure that your CSS accounts for this. Positioning of the thumb and dragging behavior is automatically mirrored by `useColorSlider`. --- ## Page: https://react-spectrum.adobe.com/react-aria/useColorSwatch.html # useColorSwatch Provides the accessibility implementation for a color swatch component. A color swatch displays a preview of a selected color. <table><tbody><tr><th>install</th><td><code>yarn add react-aria</code></td></tr><tr><th>version</th><td>3.41.1</td></tr><tr><th>usage</th><td><code><span>import</span> {useColorSwatch} <span>from</span> <span>'react-aria'</span></code></td></tr></tbody></table> View ARIA pattern W3C View repository GitHub View package NPM ## API# * * * `useColorSwatch( (props: AriaColorSwatchProps )): ColorSwatchAria ` ## Features# * * * A color swatch may seem simple to build with a `<div>`, but requires additional semantics and labeling for accessibility. * **Accessible** – Includes a localized color description for screen reader users (e.g. "dark vibrant blue"). Uses the img role with a custom `aria-roledescription` of "color swatch". * **International** – Accessible color description and role description are translated into over 30 languages. ## Anatomy# * * * A color swatch consists of a color preview, which is exposed to assistive technology with a localized color description. `useColorSwatch` returns props that you should spread onto the color swatch element, along with the parsed color value: | Name | Type | Description | | --- | --- | --- | | `colorSwatchProps` | `HTMLAttributes<HTMLElement>` | Props for the color swatch element. | | `color` | ` Color ` | The parsed color value of the swatch. | ## Example# * * * This example renders a color swatch component, with a checkerboard pattern behind partially transparent colors. import {AriaColorSwatchProps, useColorSwatch} from 'react-aria'; function ColorSwatch(props: AriaColorSwatchProps) { let { colorSwatchProps, color } = useColorSwatch(props); return ( <div {...colorSwatchProps} style={{ ...colorSwatchProps.style, width: 32, height: 32, borderRadius: 4, background: `linear-gradient(${color}, ${color}), repeating-conic-gradient(#CCC 0% 25%, white 0% 50%) 50% / 16px 16px` }} /> ); } <ColorSwatch color="#f00a" /> import { AriaColorSwatchProps, useColorSwatch } from 'react-aria'; function ColorSwatch(props: AriaColorSwatchProps) { let { colorSwatchProps, color } = useColorSwatch(props); return ( <div {...colorSwatchProps} style={{ ...colorSwatchProps.style, width: 32, height: 32, borderRadius: 4, background: `linear-gradient(${color}, ${color}), repeating-conic-gradient(#CCC 0% 25%, white 0% 50%) 50% / 16px 16px` }} /> ); } <ColorSwatch color="#f00a" /> import { AriaColorSwatchProps, useColorSwatch } from 'react-aria'; function ColorSwatch( props: AriaColorSwatchProps ) { let { colorSwatchProps, color } = useColorSwatch( props ); return ( <div {...colorSwatchProps} style={{ ...colorSwatchProps .style, width: 32, height: 32, borderRadius: 4, background: `linear-gradient(${color}, ${color}), repeating-conic-gradient(#CCC 0% 25%, white 0% 50%) 50% / 16px 16px` }} /> ); } <ColorSwatch color="#f00a" /> ## Usage# * * * The following examples show how to use the `ColorSwatch` component created in the above example. ### Value# ColorSwatch accepts a value via the `color` prop. The value should be a color string or `Color` object. This example uses the` parseColor `function to parse a color from an HSL string. import {parseColor} from 'react-stately'; <ColorSwatch color={parseColor('hsl(35, 100%, 50%)')} /> import {parseColor} from 'react-stately'; <ColorSwatch color={parseColor('hsl(35, 100%, 50%)')} /> import {parseColor} from 'react-stately'; <ColorSwatch color={parseColor( 'hsl(35, 100%, 50%)' )} /> ### Labeling# By default, useColorSwatch includes a localized color description for screen reader users (e.g. "dark vibrant blue") as an `aria-label`. If you have a more specific color name (e.g. Pantone colors), the automatically generated color description can be overridden via the `colorName` prop. An additional label describing the context of the color swatch (e.g. "Background color") can also be provided via the `aria-label` or `aria-labelledby` props. In the example below, the full accessible name of the color swatch will be "Fire truck red, Background color". <ColorSwatch color="#f00" aria-label="Background color" colorName="Fire truck red" /> <ColorSwatch color="#f00" aria-label="Background color" colorName="Fire truck red" /> <ColorSwatch color="#f00" aria-label="Background color" colorName="Fire truck red" /> --- ## Page: https://react-spectrum.adobe.com/react-aria/useColorWheel.html # useColorWheel Provides the behavior and accessibility implementation for a color wheel component. Color wheels allow users to adjust the hue of an HSL or HSB color value on a circular track. <table><tbody><tr><th>install</th><td><code>yarn add react-aria</code></td></tr><tr><th>version</th><td>3.41.1</td></tr><tr><th>usage</th><td><code><span>import</span> {useColorWheel} <span>from</span> <span>'react-aria'</span></code></td></tr></tbody></table> View ARIA pattern W3C View repository GitHub View package NPM ## API# * * * `useColorWheel( props: AriaColorWheelOptions , state: ColorWheelState , inputRef: RefObject <HTMLInputElement | | null> ): ColorWheelAria ` ## Features# * * * The <input type="color"> HTML element can be used to build a color picker, however it is very inconsistent across browsers and operating systems and consists of a complete color picker rather than only a hue color wheel. `useColorWheel` helps achieve accessible and touch-friendly color wheels that can be styled as needed. * Support for adjusting the hue of an HSL or HSB color value * Support for mouse, touch, and keyboard via the useMove hook * Multi-touch support * Pressing on the track moves the thumb to that position * Supports using the arrow keys, as well as page up/down, home, and end keys * Support for disabling the color wheel * Prevents text selection while dragging * Exposed to assistive technology as a `slider` element via ARIA * Uses a hidden native input element to support touch screen readers * Automatic ARIA labeling using the localized channel name by default ## Anatomy# * * * A color wheel consists of a circular track and a thumb that the user can drag to change the color hue. A visually hidden `<input>` element is used to represent the value to assistive technologies. `useColorWheel` returns three sets of props that you should spread onto the appropriate elements: | Name | Type | Description | | --- | --- | --- | | `trackProps` | `DOMAttributes` | Props for the track element. | | `thumbProps` | `DOMAttributes` | Props for the thumb element. | | `inputProps` | `InputHTMLAttributes<HTMLInputElement>` | Props for the visually hidden range input element. | State is managed by the `useColorWheelState` hook from `@react-stately/color`. The state object should be passed as an option to `useColorWheel` By default, `useColorWheel` provides an `aria-label` for the localized string "Hue". If you wish to override this with a more specific label, an `aria-label` or `aria-labelledby` prop may be passed instead to identify the element to assistive technologies. ## Example# * * * This example shows how to build a simple color wheel with a draggable thumb to adjust the hue value of a color. Styling for the track background and positioning of the thumb are provided by `useColorWheel` in the returned `style` prop for each element. The visually hidden `<input>` element inside the thumb is used to represent the color wheel to assistive technology. The thumb also uses the useFocusRing hook to grow in size when it is keyboard focused (try tabbing to it). import {useColorWheelState} from 'react-stately'; import {useColorWheel, useFocusRing} from 'react-aria'; const RADIUS = 100; const TRACK_THICKNESS = 28; const THUMB_SIZE = 20; function ColorWheel(props) { let { isDisabled } = props; let state = useColorWheelState(props); let inputRef = React.useRef(null); let { trackProps, inputProps, thumbProps } = useColorWheel( { ...props, outerRadius: RADIUS, innerRadius: RADIUS - TRACK_THICKNESS }, state, inputRef ); let { focusProps, isFocusVisible } = useFocusRing(); return ( <div style={{ position: 'relative', display: 'inline-block' }}> <div {...trackProps} style={{ ...trackProps.style, background: isDisabled ? 'rgb(142, 142, 142)' : trackProps.style.background }} /> <div {...thumbProps} style={{ ...thumbProps.style, background: isDisabled ? 'rgb(142, 142, 142)' : state.getDisplayColor().toString('css'), border: `2px solid ${isDisabled ? 'rgb(142, 142, 142)' : 'white'}`, boxShadow: '0 0 0 1px black, inset 0 0 0 1px black', width: isFocusVisible ? TRACK_THICKNESS + 4 : THUMB_SIZE, height: isFocusVisible ? TRACK_THICKNESS + 4 : THUMB_SIZE, borderRadius: '50%', boxSizing: 'border-box' }} > <input {...inputProps} {...focusProps} ref={inputRef} /> </div> </div> ); } <ColorWheel /> import {useColorWheelState} from 'react-stately'; import {useColorWheel, useFocusRing} from 'react-aria'; const RADIUS = 100; const TRACK_THICKNESS = 28; const THUMB_SIZE = 20; function ColorWheel(props) { let { isDisabled } = props; let state = useColorWheelState(props); let inputRef = React.useRef(null); let { trackProps, inputProps, thumbProps } = useColorWheel( { ...props, outerRadius: RADIUS, innerRadius: RADIUS - TRACK_THICKNESS }, state, inputRef ); let { focusProps, isFocusVisible } = useFocusRing(); return ( <div style={{ position: 'relative', display: 'inline-block' }} > <div {...trackProps} style={{ ...trackProps.style, background: isDisabled ? 'rgb(142, 142, 142)' : trackProps.style.background }} /> <div {...thumbProps} style={{ ...thumbProps.style, background: isDisabled ? 'rgb(142, 142, 142)' : state.getDisplayColor().toString('css'), border: `2px solid ${ isDisabled ? 'rgb(142, 142, 142)' : 'white' }`, boxShadow: '0 0 0 1px black, inset 0 0 0 1px black', width: isFocusVisible ? TRACK_THICKNESS + 4 : THUMB_SIZE, height: isFocusVisible ? TRACK_THICKNESS + 4 : THUMB_SIZE, borderRadius: '50%', boxSizing: 'border-box' }} > <input {...inputProps} {...focusProps} ref={inputRef} /> </div> </div> ); } <ColorWheel /> import {useColorWheelState} from 'react-stately'; import { useColorWheel, useFocusRing } from 'react-aria'; const RADIUS = 100; const TRACK_THICKNESS = 28; const THUMB_SIZE = 20; function ColorWheel( props ) { let { isDisabled } = props; let state = useColorWheelState( props ); let inputRef = React .useRef(null); let { trackProps, inputProps, thumbProps } = useColorWheel( { ...props, outerRadius: RADIUS, innerRadius: RADIUS - TRACK_THICKNESS }, state, inputRef ); let { focusProps, isFocusVisible } = useFocusRing(); return ( <div style={{ position: 'relative', display: 'inline-block' }} > <div {...trackProps} style={{ ...trackProps .style, background: isDisabled ? 'rgb(142, 142, 142)' : trackProps .style .background }} /> <div {...thumbProps} style={{ ...thumbProps .style, background: isDisabled ? 'rgb(142, 142, 142)' : state .getDisplayColor() .toString( 'css' ), border: `2px solid ${ isDisabled ? 'rgb(142, 142, 142)' : 'white' }`, boxShadow: '0 0 0 1px black, inset 0 0 0 1px black', width: isFocusVisible ? TRACK_THICKNESS + 4 : THUMB_SIZE, height: isFocusVisible ? TRACK_THICKNESS + 4 : THUMB_SIZE, borderRadius: '50%', boxSizing: 'border-box' }} > <input {...inputProps} {...focusProps} ref={inputRef} /> </div> </div> ); } <ColorWheel /> ## Usage# * * * The following examples show how to use the `ColorWheel` component created in the above example. ### Uncontrolled# By default, `ColorWheel` is uncontrolled with a default value of red (hue = 0˚). You can change the default value using the `defaultValue` prop. <ColorWheel defaultValue="hsl(80, 100%, 50%)" /> <ColorWheel defaultValue="hsl(80, 100%, 50%)" /> <ColorWheel defaultValue="hsl(80, 100%, 50%)" /> ### Controlled# A `ColorWheel` can be made controlled using the `value` prop. The `parseColor` function is used to parse the initial color from an HSL string, stored in state. The `onChange` prop is used to update the value in state when the user drags the thumb. import {parseColor} from 'react-stately'; function Example() { let [color, setColor] = React.useState(parseColor('hsl(0, 100%, 50%)')); return ( <> <ColorWheel value={color} onChange={setColor} /> <p>Current color value: {color.toString('hsl')}</p> </> ); } import {parseColor} from 'react-stately'; function Example() { let [color, setColor] = React.useState( parseColor('hsl(0, 100%, 50%)') ); return ( <> <ColorWheel value={color} onChange={setColor} /> <p>Current color value: {color.toString('hsl')}</p> </> ); } import {parseColor} from 'react-stately'; function Example() { let [color, setColor] = React.useState( parseColor( 'hsl(0, 100%, 50%)' ) ); return ( <> <ColorWheel value={color} onChange={setColor} /> <p> Current color value:{' '} {color.toString( 'hsl' )} </p> </> ); } Current color value: hsl(0, 100%, 50%) ### onChangeEnd# The `onChangeEnd` prop can be used to handle when a user stops dragging the color wheel, whereas the `onChange` prop is called as the user drags. function Example() { let [color, setColor] = React.useState(parseColor('hsl(0, 100%, 50%)')); return ( <> <ColorWheel defaultValue={color} onChangeEnd={setColor} /> <p>Current color value: {color.toString('hsl')}</p> </> ); } function Example() { let [color, setColor] = React.useState( parseColor('hsl(0, 100%, 50%)') ); return ( <> <ColorWheel defaultValue={color} onChangeEnd={setColor} /> <p>Current color value: {color.toString('hsl')}</p> </> ); } function Example() { let [color, setColor] = React.useState( parseColor( 'hsl(0, 100%, 50%)' ) ); return ( <> <ColorWheel defaultValue={color} onChangeEnd={setColor} /> <p> Current color value:{' '} {color.toString( 'hsl' )} </p> </> ); } Current color value: hsl(0, 100%, 50%) ### Disabled# A `ColorWheel` can be disabled using the `isDisabled` prop. This prevents the thumb from being focused or dragged. It's up to you to style your color wheel to appear disabled accordingly. <ColorWheel defaultValue="hsl(80, 100%, 50%)" isDisabled /> <ColorWheel defaultValue="hsl(80, 100%, 50%)" isDisabled /> <ColorWheel defaultValue="hsl(80, 100%, 50%)" isDisabled /> ### HTML forms# ColorWheel supports the `name` prop for integration with HTML forms. The value will be submitted as a number between 0 and 360 degrees. <ColorWheel name="hue" /> <ColorWheel name="hue" /> <ColorWheel name="hue" /> ## Internationalization# * * * ### Labeling# By default, a localized string for the "hue" channel name is used as the `aria-label` for the `ColorWheel`. When a custom `aria-label` is provided, it should be localized accordingly. To get a localized channel name to use as a visual label, you can use the `color.getChannelName` method. ### Value formatting# The `aria-valuetext` of the `<input>` element is formatted according to the user's locale automatically. If you wish to display this value visually, you can use the `color.formatChannelValue` method. ### RTL# Color wheels should not be mirrored in right-to-left languages. --- ## Page: https://react-spectrum.adobe.com/react-aria/useCalendar.html # useCalendar Provides the behavior and accessibility implementation for a calendar component. A calendar displays one or more date grids and allows users to select a single date. <table><tbody><tr><th>install</th><td><code>yarn add react-aria</code></td></tr><tr><th>version</th><td>3.41.1</td></tr><tr><th>usage</th><td><code><span>import</span> {useCalendar} <span>from</span> <span>'react-aria'</span></code></td></tr></tbody></table> View repository GitHub View package NPM ## API# * * * `useCalendar<T extends DateValue >( (props: AriaCalendarProps <T>, , state: CalendarState )): CalendarAria ``useCalendarGrid( (props: AriaCalendarGridProps , , state: CalendarState | | RangeCalendarState )): CalendarGridAria ``useCalendarCell( props: AriaCalendarCellProps , state: CalendarState | | RangeCalendarState , ref: RefObject <HTMLElement | | null> ): CalendarCellAria ` ## Features# * * * There is no standalone calendar element in HTML. `<input type="date">` is close, but this is very limited in functionality, lacking in internationalization capabilities, inconsistent between browsers, and difficult to style. `useCalendar` helps achieve accessible and international calendar components that can be styled as needed. * **Flexible** – Display one or more months at once, or a custom time range for use cases like a week view. Minimum and maximum values, unavailable dates, and non-contiguous selections are supported as well. * **International** – Support for 13 calendar systems used around the world, including Gregorian, Buddhist, Islamic, Persian, and more. Locale-specific formatting, number systems, and right-to-left support are available as well. * **Accessible** – Calendar cells can be navigated and selected using the keyboard, and localized screen reader messages are included to announce when the selection and visible date range change. * **Customizable** – As with all of React Aria, the DOM structure and styling of all elements can be fully customized. Read our blog post for more details about the internationalization, accessibility, and user experience features implemented by `useCalendar`. ## Anatomy# * * * A calendar consists of a grouping element containing one or more date grids (e.g. months), and a previous and next button for navigating between date ranges. Each calendar grid consists of cells containing button elements that can be pressed and navigated to using the arrow keys to select a date. ### useCalendar# `useCalendar` returns props that you should spread onto the appropriate elements: | Name | Type | Description | | --- | --- | --- | | `calendarProps` | `DOMAttributes` | Props for the calendar grouping element. | | `nextButtonProps` | ` AriaButtonProps ` | Props for the next button. | | `prevButtonProps` | ` AriaButtonProps ` | Props for the previous button. | | `errorMessageProps` | `DOMAttributes` | Props for the error message element, if any. | | `title` | `string` | A description of the visible date range, for use in the calendar title. | ### useCalendarGrid# `useCalendarGrid` returns props for an individual grid of dates, such as one month, along with a list of formatted weekday names in the current locale for use during rendering: | Name | Type | Description | | --- | --- | --- | | `gridProps` | `DOMAttributes` | Props for the date grid element (e.g. `<table>`). | | `headerProps` | `DOMAttributes` | Props for the grid header element (e.g. `<thead>`). | | `weekDays` | `string[]` | A list of week day abbreviations formatted for the current locale, typically used in column headers. | | `weeksInMonth` | `number` | The number of weeks in the month. | ### useCalendarCell# `useCalendarCell` returns props for an individual cell, along with states and information useful during rendering: | Name | Type | Description | | --- | --- | --- | | `cellProps` | `DOMAttributes` | Props for the grid cell element (e.g. `<td>`). | | `buttonProps` | `DOMAttributes` | Props for the button element within the cell. | | `isPressed` | `boolean` | Whether the cell is currently being pressed. | | `isSelected` | `boolean` | Whether the cell is selected. | | `isFocused` | `boolean` | Whether the cell is focused. | | `isDisabled` | `boolean` | Whether the cell is disabled, according to the calendar's `minValue`, `maxValue`, and `isDisabled` props. Disabled dates are not focusable, and cannot be selected by the user. They are typically displayed with a dimmed appearance. | | `isUnavailable` | `boolean` | Whether the cell is unavailable, according to the calendar's `isDateUnavailable` prop. Unavailable dates remain focusable, but cannot be selected by the user. They should be displayed with a visual affordance to indicate they are unavailable, such as a different color or a strikethrough. Note that because they are focusable, unavailable dates must meet a 4.5:1 color contrast ratio, as defined by WCAG. | | `isOutsideVisibleRange` | `boolean` | Whether the cell is outside the visible range of the calendar. For example, dates before the first day of a month in the same week. | | `isInvalid` | `boolean` | Whether the cell is part of an invalid selection. | | `formattedDate` | `string` | The day number formatted according to the current locale. | State is managed by the `useCalendarState` hook from `@react-stately/calendar`. The state object should be passed as an option to `useCalendar`, `useCalendarGrid`, and `useCalendarCell`. Note that much of this anatomy is shared with range calendars. The only difference is that `useCalendarState` is used instead of `useRangeCalendarState`, and `useCalendar` is used instead of `useRangeCalendar`. ## Date and time values# * * * Dates are represented in many different ways by cultures around the world. This includes differences in calendar systems, date formatting, numbering systems, weekday and weekend rules, and much more. When building applications that support users around the world, it is important to handle these aspects correctly for each locale. `useCalendar` uses the @internationalized/date library to represent dates and times. This package provides a library of objects and functions to perform date and time related manipulation, queries, and conversions that work across locales and calendars. Date and time objects can be converted to and from native JavaScript `Date` objects or ISO 8601 strings. See the documentation, or the examples below for more details. `useCalendarState` requires a `createCalendar` function to be provided, which is used to implement date manipulation across multiple calendar systems. The default implementation in `@internationalized/date` includes all supported calendar systems. While this library is quite small (8 kB minified + Brotli), you can reduce its bundle size further by providing your own implementation that includes only your supported calendars. See below for an example. ## Example# * * * A `Calendar` consists of three components: the main calendar wrapper element with previous and next buttons for navigating, one or more `CalendarGrid` components to display each month, and `CalendarCell` components for each date cell. We'll go through them one by one. For simplicity, this example only displays a single month at a time. See the styled examples section for more examples with multiple months, as well as other time ranges like weeks. import {useCalendar, useLocale} from 'react-aria'; import {useCalendarState} from 'react-stately'; import {createCalendar} from '@internationalized/date'; // Reuse the Button from your component library. See below for details. import {Button} from 'your-component-library'; function Calendar(props) { let { locale } = useLocale(); let state = useCalendarState({ createCalendar, ...props, locale }); let { calendarProps, prevButtonProps, nextButtonProps, title } = useCalendar( props, state ); return ( <div {...calendarProps} className="calendar"> <div className="header"> <h2>{title}</h2> <Button {...prevButtonProps}><</Button> <Button {...nextButtonProps}>></Button> </div> <CalendarGrid state={state} firstDayOfWeek={props.firstDayOfWeek} /> </div> ); } import {useCalendar, useLocale} from 'react-aria'; import {useCalendarState} from 'react-stately'; import {createCalendar} from '@internationalized/date'; // Reuse the Button from your component library. See below for details. import {Button} from 'your-component-library'; function Calendar(props) { let { locale } = useLocale(); let state = useCalendarState({ createCalendar, ...props, locale }); let { calendarProps, prevButtonProps, nextButtonProps, title } = useCalendar(props, state); return ( <div {...calendarProps} className="calendar"> <div className="header"> <h2>{title}</h2> <Button {...prevButtonProps}><</Button> <Button {...nextButtonProps}>></Button> </div> <CalendarGrid state={state} firstDayOfWeek={props.firstDayOfWeek} /> </div> ); } import { useCalendar, useLocale } from 'react-aria'; import {useCalendarState} from 'react-stately'; import {createCalendar} from '@internationalized/date'; // Reuse the Button from your component library. See below for details. import {Button} from 'your-component-library'; function Calendar( props ) { let { locale } = useLocale(); let state = useCalendarState({ createCalendar, ...props, locale }); let { calendarProps, prevButtonProps, nextButtonProps, title } = useCalendar( props, state ); return ( <div {...calendarProps} className="calendar" > <div className="header"> <h2>{title}</h2> <Button {...prevButtonProps} > < </Button> <Button {...nextButtonProps} > > </Button> </div> <CalendarGrid state={state} firstDayOfWeek={props .firstDayOfWeek} /> </div> ); } ### CalendarGrid# The `CalendarGrid` component will be responsible for rendering an individual month. It is a separate component so that you can render more than one month at a time if you like. It's rendered as an HTML `<table>` element, and React Aria takes care of adding the proper ARIA roles and event handlers to make it behave as an ARIA grid. You can use the arrow keys to navigate between cells, and the Enter key to select a date. The `state.getDatesInWeek` function returns the dates in each week of the month. Note that this always includes 7 values, but some of them may be null, which indicates that the date doesn't exist within the calendar system. You should render a placeholder `<td>` element in this case so that the cells line up correctly. **Note**: this component is the same as the `CalendarGrid` component shown in the useRangeCalendar docs, and you can reuse it between both `Calendar` and `RangeCalendar`. import {useCalendarGrid} from 'react-aria'; function CalendarGrid({ state, ...props }) { let { gridProps, headerProps, weekDays, weeksInMonth } = useCalendarGrid( props, state ); return ( <table {...gridProps}> <thead {...headerProps}> <tr> {weekDays.map((day, index) => <th key={index}>{day}</th>)} </tr> </thead> <tbody> {[...new Array(weeksInMonth).keys()].map((weekIndex) => ( <tr key={weekIndex}> {state.getDatesInWeek(weekIndex).map((date, i) => ( date ? ( <CalendarCell key={i} state={state} date={date} /> ) : <td key={i} /> ))} </tr> ))} </tbody> </table> ); } import {useCalendarGrid} from 'react-aria'; function CalendarGrid({ state, ...props }) { let { gridProps, headerProps, weekDays, weeksInMonth } = useCalendarGrid(props, state); return ( <table {...gridProps}> <thead {...headerProps}> <tr> {weekDays.map((day, index) => ( <th key={index}>{day}</th> ))} </tr> </thead> <tbody> {[...new Array(weeksInMonth).keys()].map( (weekIndex) => ( <tr key={weekIndex}> {state.getDatesInWeek(weekIndex).map(( date, i ) => ( date ? ( <CalendarCell key={i} state={state} date={date} /> ) : <td key={i} /> ))} </tr> ) )} </tbody> </table> ); } import {useCalendarGrid} from 'react-aria'; function CalendarGrid( { state, ...props } ) { let { gridProps, headerProps, weekDays, weeksInMonth } = useCalendarGrid( props, state ); return ( <table {...gridProps} > <thead {...headerProps} > <tr> {weekDays.map(( day, index ) => ( <th key={index} > {day} </th> ))} </tr> </thead> <tbody> {[...new Array( weeksInMonth ).keys()].map( (weekIndex) => ( <tr key={weekIndex} > {state .getDatesInWeek( weekIndex ).map(( date, i ) => ( date ? ( <CalendarCell key={i} state={state} date={date} /> ) : ( <td key={i} /> ) ))} </tr> ) )} </tbody> </table> ); } ### CalendarCell# Finally, the `CalendarCell` component renders an individual cell in a calendar. It consists of two elements: a `<td>` to represent the grid cell, and a `<div>` to represent a button that can be clicked to select the date. The `useCalendarCell` hook also returns the formatted date string in the current locale, as well as some information about the cell's state that can be useful for styling. See above for details. **Note**: this component is the same as the `CalendarCell` component shown in the useRangeCalendar docs, and you can reuse it between both `Calendar` and `RangeCalendar`. import {useCalendarCell} from 'react-aria'; function CalendarCell({ state, date }) { let ref = React.useRef(null); let { cellProps, buttonProps, isSelected, isOutsideVisibleRange, isDisabled, isUnavailable, formattedDate } = useCalendarCell({ date }, state, ref); return ( <td {...cellProps}> <div {...buttonProps} ref={ref} hidden={isOutsideVisibleRange} className={`cell ${isSelected ? 'selected' : ''} ${ isDisabled ? 'disabled' : '' } ${isUnavailable ? 'unavailable' : ''}`} > {formattedDate} </div> </td> ); } import {useCalendarCell} from 'react-aria'; function CalendarCell({ state, date }) { let ref = React.useRef(null); let { cellProps, buttonProps, isSelected, isOutsideVisibleRange, isDisabled, isUnavailable, formattedDate } = useCalendarCell({ date }, state, ref); return ( <td {...cellProps}> <div {...buttonProps} ref={ref} hidden={isOutsideVisibleRange} className={`cell ${isSelected ? 'selected' : ''} ${ isDisabled ? 'disabled' : '' } ${isUnavailable ? 'unavailable' : ''}`} > {formattedDate} </div> </td> ); } import {useCalendarCell} from 'react-aria'; function CalendarCell( { state, date } ) { let ref = React.useRef( null ); let { cellProps, buttonProps, isSelected, isOutsideVisibleRange, isDisabled, isUnavailable, formattedDate } = useCalendarCell( { date }, state, ref ); return ( <td {...cellProps}> <div {...buttonProps} ref={ref} hidden={isOutsideVisibleRange} className={`cell ${ isSelected ? 'selected' : '' } ${ isDisabled ? 'disabled' : '' } ${ isUnavailable ? 'unavailable' : '' }`} > {formattedDate} </div> </td> ); } That's it! Now we can render an example of our `Calendar` component in action. <Calendar aria-label="Event date" /> <Calendar aria-label="Event date" /> <Calendar aria-label="Event date" /> ## June 2025 <\> | S | M | T | W | T | F | S | | --- | --- | --- | --- | --- | --- | --- | | 1 | 2 | 3 | 4 | 5 | 6 | 7 | | 8 | 9 | 10 | 11 | 12 | 13 | 14 | | 15 | 16 | 17 | 18 | 19 | 20 | 21 | | 22 | 23 | 24 | 25 | 26 | 27 | 28 | | 29 | 30 | 1 | 2 | 3 | 4 | 5 | Show CSS .calendar { width: 220px; } .header { display: flex; align-items: center; gap: 4px; margin: 0 8px; } .header h2 { flex: 1; margin: 0; } .calendar table { width: 100%; } .cell { cursor: default; text-align: center; } .selected { background: var(--blue); color: white; } .unavailable { color: var(--spectrum-global-color-red-600); } .disabled { color: gray; } .calendar { width: 220px; } .header { display: flex; align-items: center; gap: 4px; margin: 0 8px; } .header h2 { flex: 1; margin: 0; } .calendar table { width: 100%; } .cell { cursor: default; text-align: center; } .selected { background: var(--blue); color: white; } .unavailable { color: var(--spectrum-global-color-red-600); } .disabled { color: gray; } .calendar { width: 220px; } .header { display: flex; align-items: center; gap: 4px; margin: 0 8px; } .header h2 { flex: 1; margin: 0; } .calendar table { width: 100%; } .cell { cursor: default; text-align: center; } .selected { background: var(--blue); color: white; } .unavailable { color: var(--spectrum-global-color-red-600); } .disabled { color: gray; } ### Button# The `Button` component is used in the above example to navigate between months. It is built using the useButton hook, and can be shared with many other components. Show code import {useButton} from 'react-aria'; function Button(props) { let ref = React.useRef(null); let { buttonProps } = useButton(props, ref); return <button {...buttonProps} ref={ref}>{props.children}</button>; } import {useButton} from 'react-aria'; function Button(props) { let ref = React.useRef(null); let { buttonProps } = useButton(props, ref); return ( <button {...buttonProps} ref={ref}> {props.children} </button> ); } import {useButton} from 'react-aria'; function Button(props) { let ref = React.useRef( null ); let { buttonProps } = useButton( props, ref ); return ( <button {...buttonProps} ref={ref} > {props.children} </button> ); } ## Styled Examples# * * * Tailwind CSS A Calendar built with Tailwind, supporting multiple visible months. Styled Components A week view component, built with Styled Components. CSS Modules A Calendar with custom month and year dropdowns, styled with CSS Modules. ## Usage# * * * The following examples show how to use the `Calendar` component created in the above example. ### Value# A `Calendar` has no selection by default. An initial, uncontrolled value can be provided to the `Calendar` using the `defaultValue` prop. Alternatively, a controlled value can be provided using the `value` prop. Date values are provided using objects in the @internationalized/date package. This library handles correct international date manipulation across calendars, time zones, and other localization concerns. `useCalendar` supports values with both date and time components, but only allows users to modify the date. By default, `useCalendar` will emit `CalendarDate` objects in the `onChange` event, but if a` CalendarDateTime `or` ZonedDateTime `object is passed as the `value` or `defaultValue`, values of that type will be emitted, changing only the date and preserving the time components. import {parseDate} from '@internationalized/date'; function Example() { let [value, setValue] = React.useState(parseDate('2020-02-03')); return ( <div style={{display: 'flex', gap: 20, flexWrap: 'wrap'}}> <Calendar aria-label="Date (uncontrolled)" defaultValue={parseDate('2020-02-03')} /> <Calendar aria-label="Date (controlled)" value={value} onChange={setValue} /> </div> ); } import {parseDate} from '@internationalized/date'; function Example() { let [value, setValue] = React.useState( parseDate('2020-02-03') ); return ( <div style={{ display: 'flex', gap: 20, flexWrap: 'wrap' }} > <Calendar aria-label="Date (uncontrolled)" defaultValue={parseDate('2020-02-03')} /> <Calendar aria-label="Date (controlled)" value={value} onChange={setValue} /> </div> ); } import {parseDate} from '@internationalized/date'; function Example() { let [value, setValue] = React.useState( parseDate( '2020-02-03' ) ); return ( <div style={{ display: 'flex', gap: 20, flexWrap: 'wrap' }} > <Calendar aria-label="Date (uncontrolled)" defaultValue={parseDate( '2020-02-03' )} /> <Calendar aria-label="Date (controlled)" value={value} onChange={setValue} /> </div> ); } ## February 2020 <\> | S | M | T | W | T | F | S | | --- | --- | --- | --- | --- | --- | --- | | 26 | 27 | 28 | 29 | 30 | 31 | 1 | | 2 | 3 | 4 | 5 | 6 | 7 | 8 | | 9 | 10 | 11 | 12 | 13 | 14 | 15 | | 16 | 17 | 18 | 19 | 20 | 21 | 22 | | 23 | 24 | 25 | 26 | 27 | 28 | 29 | ## February 2020 <\> | S | M | T | W | T | F | S | | --- | --- | --- | --- | --- | --- | --- | | 26 | 27 | 28 | 29 | 30 | 31 | 1 | | 2 | 3 | 4 | 5 | 6 | 7 | 8 | | 9 | 10 | 11 | 12 | 13 | 14 | 15 | | 16 | 17 | 18 | 19 | 20 | 21 | 22 | | 23 | 24 | 25 | 26 | 27 | 28 | 29 | ### Events# `useCalendar` accepts an `onChange` prop which is triggered whenever a date is selected by the user. The example below uses `onChange` to update a separate element with a formatted version of the date in the user's locale. This is done by converting the date to a native JavaScript `Date` object to pass to the formatter. import {useDateFormatter} from 'react-aria'; import {getLocalTimeZone} from '@internationalized/date'; function Example() { let [date, setDate] = React.useState(parseDate('2022-07-04')); let formatter = useDateFormatter({ dateStyle: 'full' }); return ( <> <Calendar aria-label="Event date" value={date} onChange={setDate} /> <p>Selected date: {formatter.format(date.toDate(getLocalTimeZone()))}</p> </> ); } import {useDateFormatter} from 'react-aria'; import {getLocalTimeZone} from '@internationalized/date'; function Example() { let [date, setDate] = React.useState( parseDate('2022-07-04') ); let formatter = useDateFormatter({ dateStyle: 'full' }); return ( <> <Calendar aria-label="Event date" value={date} onChange={setDate} /> <p> Selected date:{' '} {formatter.format(date.toDate(getLocalTimeZone()))} </p> </> ); } import {useDateFormatter} from 'react-aria'; import {getLocalTimeZone} from '@internationalized/date'; function Example() { let [date, setDate] = React.useState( parseDate( '2022-07-04' ) ); let formatter = useDateFormatter({ dateStyle: 'full' }); return ( <> <Calendar aria-label="Event date" value={date} onChange={setDate} /> <p> Selected date: {' '} {formatter .format( date.toDate( getLocalTimeZone() ) )} </p> </> ); } ## July 2022 <\> | S | M | T | W | T | F | S | | --- | --- | --- | --- | --- | --- | --- | | 26 | 27 | 28 | 29 | 30 | 1 | 2 | | 3 | 4 | 5 | 6 | 7 | 8 | 9 | | 10 | 11 | 12 | 13 | 14 | 15 | 16 | | 17 | 18 | 19 | 20 | 21 | 22 | 23 | | 24 | 25 | 26 | 27 | 28 | 29 | 30 | | 31 | 1 | 2 | 3 | 4 | 5 | 6 | Selected date: Monday, July 4, 2022 ### International calendars# `useCalendar` supports selecting dates in many calendar systems used around the world, including Gregorian, Hebrew, Indian, Islamic, Buddhist, and more. Dates are automatically displayed in the appropriate calendar system for the user's locale. The calendar system can be overridden using the Unicode calendar locale extension, passed to the I18nProvider component. Selected dates passed to `onChange` always use the same calendar system as the `value` or `defaultValue` prop. If no `value` or `defaultValue` is provided, then dates passed to `onChange` are always in the Gregorian calendar since this is the most commonly used. This means that even though the user selects dates in their local calendar system, applications are able to deal with dates from all users consistently. The below example displays a `Calendar` in the Hindi language, using the Indian calendar. Dates emitted from `onChange` are in the Gregorian calendar. import {I18nProvider} from 'react-aria'; function Example() { let [date, setDate] = React.useState(null); return ( <I18nProvider locale="hi-IN-u-ca-indian"> <Calendar aria-label="Date" value={date} onChange={setDate} /> <p>Selected date: {date?.toString()}</p> </I18nProvider> ); } import {I18nProvider} from 'react-aria'; function Example() { let [date, setDate] = React.useState(null); return ( <I18nProvider locale="hi-IN-u-ca-indian"> <Calendar aria-label="Date" value={date} onChange={setDate} /> <p>Selected date: {date?.toString()}</p> </I18nProvider> ); } import {I18nProvider} from 'react-aria'; function Example() { let [date, setDate] = React.useState(null); return ( <I18nProvider locale="hi-IN-u-ca-indian"> <Calendar aria-label="Date" value={date} onChange={setDate} /> <p> Selected date: {' '} {date ?.toString()} </p> </I18nProvider> ); } ## शक 1947 ज्येष्ठ <\> | र | सो | मं | बु | गु | शु | श | | --- | --- | --- | --- | --- | --- | --- | | 28 | 29 | 30 | 31 | 1 | 2 | 3 | | 4 | 5 | 6 | 7 | 8 | 9 | 10 | | 11 | 12 | 13 | 14 | 15 | 16 | 17 | | 18 | 19 | 20 | 21 | 22 | 23 | 24 | | 25 | 26 | 27 | 28 | 29 | 30 | 31 | Selected date: ### Custom calendar systems# `Calendar` also supports custom calendar systems that implement custom business rules. An example would be a fiscal year calendar that follows a 4-5-4 format, where month ranges don't follow the usual Gregorian calendar. The `createCalendar` prop accepts a function that returns an instance of the `Calendar` interface. See the @internationalized/date docs for an example implementation. import {GregorianCalendar} from '@internationalized/date'; function Example() { return ( <Calendar firstDayOfWeek="sun" createCalendar={() => new Custom454()} /> ); } class Custom454 extends GregorianCalendar { // See @internationalized/date docs linked above... } import {GregorianCalendar} from '@internationalized/date'; function Example() { return ( <Calendar firstDayOfWeek="sun" createCalendar={() => new Custom454()} /> ); } class Custom454 extends GregorianCalendar { // See @internationalized/date docs linked above... } import {GregorianCalendar} from '@internationalized/date'; function Example() { return ( <Calendar firstDayOfWeek="sun" createCalendar={() => new Custom454()} /> ); } class Custom454 extends GregorianCalendar { // See @internationalized/date docs linked above... } ## June 2025 <\> | S | M | T | W | T | F | S | | --- | --- | --- | --- | --- | --- | --- | | 1 | 2 | 3 | 4 | 5 | 6 | 7 | | 8 | 9 | 10 | 11 | 12 | 13 | 14 | | 15 | 16 | 17 | 18 | 19 | 20 | 21 | | 22 | 23 | 24 | 25 | 26 | 27 | 28 | ### Validation# By default, `useCalendar` allows selecting any date. The `minValue` and `maxValue` props can also be used to prevent the user from selecting dates outside a certain range. This example only accepts dates after today. import {today} from '@internationalized/date'; <Calendar aria-label="Appointment date" minValue={today(getLocalTimeZone())} /> import {today} from '@internationalized/date'; <Calendar aria-label="Appointment date" minValue={today(getLocalTimeZone())} /> import {today} from '@internationalized/date'; <Calendar aria-label="Appointment date" minValue={today( getLocalTimeZone() )} /> ## June 2025 <\> | S | M | T | W | T | F | S | | --- | --- | --- | --- | --- | --- | --- | | 1 | 2 | 3 | 4 | 5 | 6 | 7 | | 8 | 9 | 10 | 11 | 12 | 13 | 14 | | 15 | 16 | 17 | 18 | 19 | 20 | 21 | | 22 | 23 | 24 | 25 | 26 | 27 | 28 | | 29 | 30 | 1 | 2 | 3 | 4 | 5 | ### Unavailable dates# `useCalendar` supports marking certain dates as _unavailable_. These dates remain focusable with the keyboard so that navigation is consistent, but cannot be selected by the user. In this example, they are displayed in red. The `isDateUnavailable` prop accepts a callback that is called to evaluate whether each visible date is unavailable. This example includes multiple unavailable date ranges, e.g. dates when no appointments are available. In addition, all weekends are unavailable. The `minValue` prop is also used to prevent selecting dates before today. import {useLocale} from 'react-aria'; import {isWeekend, today} from '@internationalized/date'; function Example() { let now = today(getLocalTimeZone()); let disabledRanges = [ [now, now.add({ days: 5 })], [now.add({ days: 14 }), now.add({ days: 16 })], [now.add({ days: 23 }), now.add({ days: 24 })] ]; let { locale } = useLocale(); let isDateUnavailable = (date) => isWeekend(date, locale) || disabledRanges.some((interval) => date.compare(interval[0]) >= 0 && date.compare(interval[1]) <= 0 ); return ( <Calendar aria-label="Appointment date" minValue={today(getLocalTimeZone())} isDateUnavailable={isDateUnavailable} /> ); } import {useLocale} from 'react-aria'; import {isWeekend, today} from '@internationalized/date'; function Example() { let now = today(getLocalTimeZone()); let disabledRanges = [ [now, now.add({ days: 5 })], [now.add({ days: 14 }), now.add({ days: 16 })], [now.add({ days: 23 }), now.add({ days: 24 })] ]; let { locale } = useLocale(); let isDateUnavailable = (date) => isWeekend(date, locale) || disabledRanges.some((interval) => date.compare(interval[0]) >= 0 && date.compare(interval[1]) <= 0 ); return ( <Calendar aria-label="Appointment date" minValue={today(getLocalTimeZone())} isDateUnavailable={isDateUnavailable} /> ); } import {useLocale} from 'react-aria'; import { isWeekend, today } from '@internationalized/date'; function Example() { let now = today( getLocalTimeZone() ); let disabledRanges = [ [ now, now.add({ days: 5 }) ], [ now.add({ days: 14 }), now.add({ days: 16 }) ], [ now.add({ days: 23 }), now.add({ days: 24 }) ] ]; let { locale } = useLocale(); let isDateUnavailable = (date) => isWeekend( date, locale ) || disabledRanges .some(( interval ) => date.compare( interval[0] ) >= 0 && date.compare( interval[1] ) <= 0 ); return ( <Calendar aria-label="Appointment date" minValue={today( getLocalTimeZone() )} isDateUnavailable={isDateUnavailable} /> ); } ## June 2025 <\> | S | M | T | W | T | F | S | | --- | --- | --- | --- | --- | --- | --- | | 1 | 2 | 3 | 4 | 5 | 6 | 7 | | 8 | 9 | 10 | 11 | 12 | 13 | 14 | | 15 | 16 | 17 | 18 | 19 | 20 | 21 | | 22 | 23 | 24 | 25 | 26 | 27 | 28 | | 29 | 30 | 1 | 2 | 3 | 4 | 5 | ### Controlling the focused date# By default, the selected date is focused when a `Calendar` first mounts. If no `value` or `defaultValue` prop is provided, then the current date is focused. However, `useCalendar` supports controlling which date is focused using the `focusedValue` and `onFocusChange` props. This also determines which month is visible. The `defaultFocusedValue` prop allows setting the initial focused date when the `Calendar` first mounts, without controlling it. This example focuses July 1, 2021 by default. The user may change the focused date, and the `onFocusChange` event updates the state. Clicking the button resets the focused date back to the initial value. import {CalendarDate} from '@internationalized/date'; function Example() { let defaultDate = new CalendarDate(2021, 7, 1); let [focusedDate, setFocusedDate] = React.useState(defaultDate); return ( <div style={{ flexDirection: 'column', alignItems: 'start', gap: 20 }}> <button onClick={() => setFocusedDate(defaultDate)}> Reset focused date </button> <Calendar focusedValue={focusedDate} onFocusChange={setFocusedDate} /> </div> ); } import {CalendarDate} from '@internationalized/date'; function Example() { let defaultDate = new CalendarDate(2021, 7, 1); let [focusedDate, setFocusedDate] = React.useState( defaultDate ); return ( <div style={{ flexDirection: 'column', alignItems: 'start', gap: 20 }} > <button onClick={() => setFocusedDate(defaultDate)}> Reset focused date </button> <Calendar focusedValue={focusedDate} onFocusChange={setFocusedDate} /> </div> ); } import {CalendarDate} from '@internationalized/date'; function Example() { let defaultDate = new CalendarDate( 2021, 7, 1 ); let [ focusedDate, setFocusedDate ] = React.useState( defaultDate ); return ( <div style={{ flexDirection: 'column', alignItems: 'start', gap: 20 }} > <button onClick={() => setFocusedDate( defaultDate )} > Reset focused date </button> <Calendar focusedValue={focusedDate} onFocusChange={setFocusedDate} /> </div> ); } Reset focused date ## July 2021 <\> | S | M | T | W | T | F | S | | --- | --- | --- | --- | --- | --- | --- | | 27 | 28 | 29 | 30 | 1 | 2 | 3 | | 4 | 5 | 6 | 7 | 8 | 9 | 10 | | 11 | 12 | 13 | 14 | 15 | 16 | 17 | | 18 | 19 | 20 | 21 | 22 | 23 | 24 | | 25 | 26 | 27 | 28 | 29 | 30 | 31 | ### Disabled# The `isDisabled` boolean prop makes the Calendar disabled. Cells cannot be focused or selected. <Calendar aria-label="Event date" isDisabled /> <Calendar aria-label="Event date" isDisabled /> <Calendar aria-label="Event date" isDisabled /> ## June 2025 <\> | S | M | T | W | T | F | S | | --- | --- | --- | --- | --- | --- | --- | | 1 | 2 | 3 | 4 | 5 | 6 | 7 | | 8 | 9 | 10 | 11 | 12 | 13 | 14 | | 15 | 16 | 17 | 18 | 19 | 20 | 21 | | 22 | 23 | 24 | 25 | 26 | 27 | 28 | | 29 | 30 | 1 | 2 | 3 | 4 | 5 | ### Read only# The `isReadOnly` boolean prop makes the Calendar's value immutable. Unlike `isDisabled`, the Calendar remains focusable. <Calendar aria-label="Event date" value={today(getLocalTimeZone())} isReadOnly /> <Calendar aria-label="Event date" value={today(getLocalTimeZone())} isReadOnly /> <Calendar aria-label="Event date" value={today( getLocalTimeZone() )} isReadOnly /> ## June 2025 <\> | S | M | T | W | T | F | S | | --- | --- | --- | --- | --- | --- | --- | | 1 | 2 | 3 | 4 | 5 | 6 | 7 | | 8 | 9 | 10 | 11 | 12 | 13 | 14 | | 15 | 16 | 17 | 18 | 19 | 20 | 21 | | 22 | 23 | 24 | 25 | 26 | 27 | 28 | | 29 | 30 | 1 | 2 | 3 | 4 | 5 | ### Custom first day of week# By default, the first day of the week is automatically set based on the current locale. This can be changed by setting the `firstDayOfWeek` prop to `'sun'`, `'mon'`, `'tue'`, `'wed'`, `'thu'`, `'fri'`, or `'sat'`. <Calendar aria-label="Event date" value={today(getLocalTimeZone())} firstDayOfWeek="mon" /> <Calendar aria-label="Event date" value={today(getLocalTimeZone())} firstDayOfWeek="mon" /> <Calendar aria-label="Event date" value={today( getLocalTimeZone() )} firstDayOfWeek="mon" /> ## June 2025 <\> | M | T | W | T | F | S | S | | --- | --- | --- | --- | --- | --- | --- | | 26 | 27 | 28 | 29 | 30 | 31 | 1 | | 2 | 3 | 4 | 5 | 6 | 7 | 8 | | 9 | 10 | 11 | 12 | 13 | 14 | 15 | | 16 | 17 | 18 | 19 | 20 | 21 | 22 | | 23 | 24 | 25 | 26 | 27 | 28 | 29 | | 30 | 1 | 2 | 3 | 4 | 5 | 6 | ### Labeling# An aria-label must be provided to the `Calendar` for accessibility. If it is labeled by a separate element, an `aria-labelledby` prop must be provided using the `id` of the labeling element instead. ### Internationalization# In order to internationalize a `Calendar`, a localized string should be passed to the `aria-label` prop. For languages that are read right-to-left (e.g. Hebrew and Arabic), keyboard navigation is automatically flipped. Ensure that your CSS accounts for this as well. Dates are automatically formatted using the current locale. ## Advanced topics# * * * ### Reducing bundle size# In the example above, the `createCalendar` function from the @internationalized/date package is passed to the` useCalendarState `hook. This function receives a calendar identifier string, and provides` Calendar `instances to React Stately, which are used to implement date manipulation. By default, this includes all calendar systems supported by `@internationalized/date`. However, if your application supports a more limited set of regions, or you know you will only be picking dates in a certain calendar system, you can reduce your bundle size by providing your own implementation of `createCalendar` that includes a subset of these `Calendar` implementations. For example, if your application only supports Gregorian dates, you could implement a `createCalendar` function like this: import {useLocale} from 'react-aria'; import {useCalendarState} from 'react-stately'; import {GregorianCalendar} from '@internationalized/date'; function createCalendar(identifier) { switch (identifier) { case 'gregory': return new GregorianCalendar(); default: throw new Error(`Unsupported calendar ${identifier}`); } } function Calendar(props) { let { locale } = useLocale(); let state = useCalendarState({ ...props, locale, createCalendar }); // ... } import {useLocale} from 'react-aria'; import {useCalendarState} from 'react-stately'; import {GregorianCalendar} from '@internationalized/date'; function createCalendar(identifier) { switch (identifier) { case 'gregory': return new GregorianCalendar(); default: throw new Error(`Unsupported calendar ${identifier}`); } } function Calendar(props) { let { locale } = useLocale(); let state = useCalendarState({ ...props, locale, createCalendar }); // ... } import {useLocale} from 'react-aria'; import {useCalendarState} from 'react-stately'; import {GregorianCalendar} from '@internationalized/date'; function createCalendar( identifier ) { switch (identifier) { case 'gregory': return new GregorianCalendar(); default: throw new Error( `Unsupported calendar ${identifier}` ); } } function Calendar( props ) { let { locale } = useLocale(); let state = useCalendarState({ ...props, locale, createCalendar }); // ... } This way, only `GregorianCalendar` is imported, and the other calendar implementations can be tree-shaken. See the Calendar documentation in `@internationalized/date` to learn more about the supported calendar systems, and a list of string identifiers. --- ## Page: https://react-spectrum.adobe.com/react-aria/useDateField.html # useDateField Provides the behavior and accessibility implementation for a date field component. A date field allows users to enter and edit date and time values using a keyboard. Each part of a date value is displayed in an individually editable segment. <table><tbody><tr><th>install</th><td><code>yarn add react-aria</code></td></tr><tr><th>version</th><td>3.41.1</td></tr><tr><th>usage</th><td><code><span>import</span> {useDateField} <span>from</span> <span>'react-aria'</span></code></td></tr></tbody></table> View repository GitHub View package NPM ## API# * * * `useDateField<T extends DateValue >( props: AriaDateFieldOptions <T>, state: DateFieldState , ref: RefObject <Element | | null> ): DateFieldAria ``useDateSegment( segment: DateSegment , state: DateFieldState , ref: RefObject <HTMLElement | | null> ): DateSegmentAria ` ## Features# * * * A date field can be built using `<input type="date">`, but this is very limited in functionality, lacking in internationalization capabilities, inconsistent between browsers, and difficult to style. `useDateField` helps achieve accessible and international date and time fields that can be styled as needed. * **Dates and times** – Support for dates and times with configurable granularity. * **International** – Support for 13 calendar systems used around the world, including Gregorian, Buddhist, Islamic, Persian, and more. Locale-specific formatting, number systems, hour cycles, and right-to-left support are available as well. * **Time zone aware** – Dates and times can optionally include a time zone. All modifications follow time zone rules such as daylight saving time. * **Accessible** – Each date and time unit is displayed as an individually focusable and editable segment, which allows users an easy way to edit dates using the keyboard, in any date format and locale. * **Touch friendly** – Date segments are editable using an easy to use numeric keypad, and all interactions are accessible using touch-based screen readers. * **Customizable** – As with all of React Aria, the DOM structure and styling of all elements can be fully customized. Read our blog post for more details about the internationalization, accessibility, and user experience features implemented by `useDateField`. ## Anatomy# * * * A date field consists of a label, and a group of segments representing each unit of a date and time (e.g. years, months, days, etc.). Each segment is individually focusable and editable by the user, by typing or using the arrow keys to increment and decrement the value. This approach allows values to be formatted and parsed correctly regardless of the locale or date format, and offers an easy and error-free way to edit dates using the keyboard. `useDateField` also supports optional description and error message elements, which can be used to provide more context about the field, and any validation messages. These are linked with the input via the `aria-describedby` attribute. `useDateField` returns props that you should spread onto the appropriate elements: | Name | Type | Description | | --- | --- | --- | | `labelProps` | `DOMAttributes` | Props for the field's visible label element, if any. | | `fieldProps` | ` GroupDOMAttributes ` | Props for the field grouping element. | | `inputProps` | `InputHTMLAttributes<HTMLInputElement>` | Props for the hidden input element for HTML form submission. | | `descriptionProps` | `DOMAttributes` | Props for the description element, if any. | | `errorMessageProps` | `DOMAttributes` | Props for the error message element, if any. | | `isInvalid` | `boolean` | Whether the input value is invalid. | | `validationErrors` | `string[]` | The current error messages for the input if it is invalid, otherwise an empty array. | | `validationDetails` | `ValidityState` | The native validation details for the input. | `useDateSegment` returns props for an individual date segment: | Name | Type | Description | | --- | --- | --- | | `segmentProps` | `React.HTMLAttributes<HTMLDivElement>` | Props for the segment element. | Note that most of this anatomy is shared with useTimeField, so you can reuse many components between them if you have both. State is managed by the `useDateFieldState` hook from `@react-stately/datepicker`. The state object should be passed as an option to `useDateField` and `useDateSegment`. If the date field does not have a visible label, an `aria-label` or `aria-labelledby` prop must be passed instead to identify it to assistive technology. ## Date and time values# * * * Dates and times are represented in many different ways by cultures around the world. This includes differences in calendar systems, time zones, daylight saving time rules, date and time formatting, weekday and weekend rules, and much more. When building applications that support users around the world, it is important to handle these aspects correctly for each locale. `useDateField` uses the @internationalized/date library to represent dates and times. This package provides a library of objects and functions to perform date and time related manipulation, queries, and conversions that work across locales and calendars. Date and time objects can be converted to and from native JavaScript `Date` objects or ISO 8601 strings. See the documentation, or the examples below for more details. `useDateFieldState` requires a `createCalendar` function to be provided, which is used to implement date manipulation across multiple calendar systems. The default implementation in `@internationalized/date` includes all supported calendar systems. While this library is quite small (8 kB minified + Brotli), you can reduce its bundle size further by providing your own implementation that includes only your supported calendars. See below for an example. ## Example# * * * import {useDateField, useDateSegment, useLocale} from 'react-aria'; import {useDateFieldState} from 'react-stately'; import {createCalendar} from '@internationalized/date'; export function DateField(props) { let { locale } = useLocale(); let state = useDateFieldState({ ...props, locale, createCalendar }); let ref = React.useRef(null); let { labelProps, fieldProps } = useDateField(props, state, ref); return ( <div className="wrapper"> <span {...labelProps}>{props.label}</span> <div {...fieldProps} ref={ref} className="field"> {state.segments.map((segment, i) => ( <DateSegment key={i} segment={segment} state={state} /> ))} {state.isInvalid && <span aria-hidden="true">🚫</span>} </div> </div> ); } function DateSegment({ segment, state }) { let ref = React.useRef(null); let { segmentProps } = useDateSegment(segment, state, ref); return ( <span {...segmentProps} ref={ref} className={`segment ${segment.isPlaceholder ? 'placeholder' : ''}`} > {segment.text} </span> ); } <DateField label="Event date" /> import { useDateField, useDateSegment, useLocale } from 'react-aria'; import {useDateFieldState} from 'react-stately'; import {createCalendar} from '@internationalized/date'; export function DateField(props) { let { locale } = useLocale(); let state = useDateFieldState({ ...props, locale, createCalendar }); let ref = React.useRef(null); let { labelProps, fieldProps } = useDateField( props, state, ref ); return ( <div className="wrapper"> <span {...labelProps}>{props.label}</span> <div {...fieldProps} ref={ref} className="field"> {state.segments.map((segment, i) => ( <DateSegment key={i} segment={segment} state={state} /> ))} {state.isInvalid && <span aria-hidden="true">🚫</span>} </div> </div> ); } function DateSegment({ segment, state }) { let ref = React.useRef(null); let { segmentProps } = useDateSegment( segment, state, ref ); return ( <span {...segmentProps} ref={ref} className={`segment ${ segment.isPlaceholder ? 'placeholder' : '' }`} > {segment.text} </span> ); } <DateField label="Event date" /> import { useDateField, useDateSegment, useLocale } from 'react-aria'; import {useDateFieldState} from 'react-stately'; import {createCalendar} from '@internationalized/date'; export function DateField( props ) { let { locale } = useLocale(); let state = useDateFieldState({ ...props, locale, createCalendar }); let ref = React.useRef( null ); let { labelProps, fieldProps } = useDateField( props, state, ref ); return ( <div className="wrapper"> <span {...labelProps} > {props.label} </span> <div {...fieldProps} ref={ref} className="field" > {state.segments .map(( segment, i ) => ( <DateSegment key={i} segment={segment} state={state} /> ))} {state .isInvalid && ( <span aria-hidden="true"> 🚫 </span> )} </div> </div> ); } function DateSegment( { segment, state } ) { let ref = React.useRef( null ); let { segmentProps } = useDateSegment( segment, state, ref ); return ( <span {...segmentProps} ref={ref} className={`segment ${ segment .isPlaceholder ? 'placeholder' : '' }`} > {segment.text} </span> ); } <DateField label="Event date" /> Event date mm/dd/yyyy Show CSS .wrapper { display: flex; flex-direction: column; align-items: flex-start; } .field { display: block; padding: 2px 4px; border-radius: 2px; border: 1px solid var(--gray); background: var(--spectrum-global-color-gray-50); } .field:focus-within { border-color: var(--blue); } .segment { padding: 0 2px; font-variant-numeric: tabular-nums; text-align: end; } .segment.placeholder { color: var(--spectrum-gray-600); } .segment:focus { color: white; background: var(--blue); outline: none; border-radius: 2px; } .wrapper { display: flex; flex-direction: column; align-items: flex-start; } .field { display: block; padding: 2px 4px; border-radius: 2px; border: 1px solid var(--gray); background: var(--spectrum-global-color-gray-50); } .field:focus-within { border-color: var(--blue); } .segment { padding: 0 2px; font-variant-numeric: tabular-nums; text-align: end; } .segment.placeholder { color: var(--spectrum-gray-600); } .segment:focus { color: white; background: var(--blue); outline: none; border-radius: 2px; } .wrapper { display: flex; flex-direction: column; align-items: flex-start; } .field { display: block; padding: 2px 4px; border-radius: 2px; border: 1px solid var(--gray); background: var(--spectrum-global-color-gray-50); } .field:focus-within { border-color: var(--blue); } .segment { padding: 0 2px; font-variant-numeric: tabular-nums; text-align: end; } .segment.placeholder { color: var(--spectrum-gray-600); } .segment:focus { color: white; background: var(--blue); outline: none; border-radius: 2px; } ## Styled examples# * * * Tailwind CSS A date and time field built with Tailwind and React Aria. ## Usage# * * * The following examples show how to use the `DateField` component created in the above example. ### Value# A `DateField` displays a placeholder by default. An initial, uncontrolled value can be provided to the `DateField` using the `defaultValue` prop. Alternatively, a controlled value can be provided using the `value` prop. Date values are provided using objects in the @internationalized/date package. This library handles correct international date manipulation across calendars, time zones, and other localization concerns. `useDateField` supports values of the following types: * ` CalendarDate `– a date without any time components. May be parsed from a string representation using the` parseDate `function. Use this type to represent dates where the time is not important, such as a birthday or an all day calendar event. * ` CalendarDateTime `– a date with a time, but not in any specific time zone. May be parsed from a string representation using the` parseDateTime `function. Use this type to represent times that occur at the same local time regardless of the time zone, such as the time of New Years Eve fireworks which always occur at midnight. Most times are better stored as a `ZonedDateTime`. * ` ZonedDateTime `– a date with a time in a specific time zone. May be parsed from a string representation using the` parseZonedDateTime `,` parseAbsolute `, or` parseAbsoluteToLocal `functions. Use this type to represent an exact moment in time at a particular location on Earth. import {parseDate} from '@internationalized/date'; function Example() { let [value, setValue] = React.useState(parseDate('2020-02-03')); return ( <> <DateField label="Date (uncontrolled)" defaultValue={parseDate('2020-02-03')} /> <DateField label="Date (controlled)" value={value} onChange={setValue} /> </> ); } import {parseDate} from '@internationalized/date'; function Example() { let [value, setValue] = React.useState( parseDate('2020-02-03') ); return ( <> <DateField label="Date (uncontrolled)" defaultValue={parseDate('2020-02-03')} /> <DateField label="Date (controlled)" value={value} onChange={setValue} /> </> ); } import {parseDate} from '@internationalized/date'; function Example() { let [value, setValue] = React.useState( parseDate( '2020-02-03' ) ); return ( <> <DateField label="Date (uncontrolled)" defaultValue={parseDate( '2020-02-03' )} /> <DateField label="Date (controlled)" value={value} onChange={setValue} /> </> ); } Date (uncontrolled) 2/3/2020 Date (controlled) 2/3/2020 ### Events# `useDateField` accepts an `onChange` prop which is triggered whenever the date is edited by the user. The example below uses `onChange` to update a separate element with a formatted version of the date in the user's locale and local time zone. This is done by converting the date to a native JavaScript `Date` object to pass to the formatter. import {useDateFormatter} from 'react-aria'; import {getLocalTimeZone} from '@internationalized/date'; function Example() { let [date, setDate] = React.useState(parseDate('1985-07-03')); let formatter = useDateFormatter({ dateStyle: 'full' }); return ( <> <DateField label="Birth date" value={date} onChange={setDate} /> <p> Selected date:{' '} {date ? formatter.format(date.toDate(getLocalTimeZone())) : '--'} </p> </> ); } import {useDateFormatter} from 'react-aria'; import {getLocalTimeZone} from '@internationalized/date'; function Example() { let [date, setDate] = React.useState( parseDate('1985-07-03') ); let formatter = useDateFormatter({ dateStyle: 'full' }); return ( <> <DateField label="Birth date" value={date} onChange={setDate} /> <p> Selected date: {date ? formatter.format( date.toDate(getLocalTimeZone()) ) : '--'} </p> </> ); } import {useDateFormatter} from 'react-aria'; import {getLocalTimeZone} from '@internationalized/date'; function Example() { let [date, setDate] = React.useState( parseDate( '1985-07-03' ) ); let formatter = useDateFormatter({ dateStyle: 'full' }); return ( <> <DateField label="Birth date" value={date} onChange={setDate} /> <p> Selected date: {' '} {date ? formatter .format( date .toDate( getLocalTimeZone() ) ) : '--'} </p> </> ); } Birth date 7/3/1985 Selected date: Wednesday, July 3, 1985 ### Time zones# `useDateField` is time zone aware when a `ZonedDateTime` object is provided as the value. In this case, the time zone abbreviation is displayed, and time zone concerns such as daylight saving time are taken into account when the value is manipulated. In most cases, your data will come from and be sent to a server as an ISO 8601 formatted string. @internationalized/date includes functions for parsing strings in multiple formats into `ZonedDateTime` objects. Which format you use will depend on what information you need to store. * ` parseZonedDateTime `– This function parses a date with an explicit time zone and optional UTC offset attached (e.g. `"2021-11-07T00:45[America/Los_Angeles]"` or `"2021-11-07T00:45-07:00[America/Los_Angeles]"`). This format preserves the maximum amount of information. If the exact local time and time zone that a user selected is important, use this format. Storing the time zone and offset that was selected rather than converting to UTC ensures that the local time is correct regardless of daylight saving rule changes (e.g. if a locale abolishes DST). Examples where this applies include calendar events, reminders, and other times that occur in a particular location. * ` parseAbsolute `– This function parses an absolute date and time that occurs at the same instant at all locations on Earth. It can be represented in UTC (e.g. `"2021-11-07T07:45:00Z"`), or stored with a particular offset (e.g. `"2021-11-07T07:45:00-07:00"`). A time zone identifier, e.g. `America/Los_Angeles`, must be passed, and the result will be converted into that time zone. Absolute times are the best way to represent events that occurred in the past, or future events where an exact time is needed, regardless of time zone. * ` parseAbsoluteToLocal `– This function parses an absolute date and time into the current user's local time zone. It is a shortcut for `parseAbsolute`, and accepts the same formats. import {parseZonedDateTime} from '@internationalized/date'; <DateField label="Event date" defaultValue={parseZonedDateTime('2022-11-07T00:45[America/Los_Angeles]')} /> import {parseZonedDateTime} from '@internationalized/date'; <DateField label="Event date" defaultValue={parseZonedDateTime( '2022-11-07T00:45[America/Los_Angeles]' )} /> import {parseZonedDateTime} from '@internationalized/date'; <DateField label="Event date" defaultValue={parseZonedDateTime( '2022-11-07T00:45[America/Los_Angeles]' )} /> Event date 11/7/2022, 12:45 AM PST `useDateField` displays times in the time zone included in the `ZonedDateTime` object. The above example is always displayed in Pacific Standard Time because the `America/Los_Angeles` time zone identifier is provided. @internationalized/date includes functions for converting dates between time zones, or parsing a date directly into a specific time zone or the user's local time zone, as shown below. import {parseAbsoluteToLocal} from '@internationalized/date'; <DateField label="Event date" defaultValue={parseAbsoluteToLocal('2021-11-07T07:45:00Z')} /> import {parseAbsoluteToLocal} from '@internationalized/date'; <DateField label="Event date" defaultValue={parseAbsoluteToLocal( '2021-11-07T07:45:00Z' )} /> import {parseAbsoluteToLocal} from '@internationalized/date'; <DateField label="Event date" defaultValue={parseAbsoluteToLocal( '2021-11-07T07:45:00Z' )} /> Event date 11/7/2021, 7:45 AM UTC ### Granularity# The `granularity` prop allows you to control the smallest unit that is displayed by `useDateField`. By default, `CalendarDate` values are displayed with `"day"` granularity (year, month, and day), and `CalendarDateTime` and `ZonedDateTime` values are displayed with `"minute"` granularity. More granular time values can be displayed by setting the `granularity` prop to `"second"`. In addition, when a value with a time is provided but you wish to only display the date, you can set the granularity to `"day"`. This has no effect on the actual value (it still has a time component), only on what fields are displayed. In the following example, two DateFields are synchronized with the same value, but display different granularities. function Example() { let [date, setDate] = React.useState( parseAbsoluteToLocal('2021-04-07T18:45:22Z') ); return ( <> <DateField label="Date and time" granularity="second" value={date} onChange={setDate} /> <DateField label="Date" granularity="day" value={date} onChange={setDate} /> </> ); } function Example() { let [date, setDate] = React.useState( parseAbsoluteToLocal('2021-04-07T18:45:22Z') ); return ( <> <DateField label="Date and time" granularity="second" value={date} onChange={setDate} /> <DateField label="Date" granularity="day" value={date} onChange={setDate} /> </> ); } function Example() { let [date, setDate] = React.useState( parseAbsoluteToLocal( '2021-04-07T18:45:22Z' ) ); return ( <> <DateField label="Date and time" granularity="second" value={date} onChange={setDate} /> <DateField label="Date" granularity="day" value={date} onChange={setDate} /> </> ); } Date and time 4/7/2021, 6:45:22 PM UTC Date 4/7/2021 If no `value` or `defaultValue` prop is passed, then the `granularity` prop also affects which type of value is emitted from the `onChange` event. Note that by default, time values will not have a time zone because none was supplied. You can override this by setting the `placeholderValue` prop explicitly. Values emitted from `onChange` will use the time zone of the placeholder value. import {now} from '@internationalized/date'; <DateField label="Event date" granularity="second" /> <DateField label="Event date" placeholderValue={now('America/New_York')} granularity="second" /> import {now} from '@internationalized/date'; <DateField label="Event date" granularity="second" /> <DateField label="Event date" placeholderValue={now('America/New_York')} granularity="second" /> import {now} from '@internationalized/date'; <DateField label="Event date" granularity="second" /> <DateField label="Event date" placeholderValue={now( 'America/New_York' )} granularity="second" /> Event date mm/dd/yyyy, ––:––:–– AM Event date mm/dd/yyyy, ––:––:–– AM EDT ### International calendars# `useDateField` supports selecting dates in many calendar systems used around the world, including Gregorian, Hebrew, Indian, Islamic, Buddhist, and more. Dates are automatically displayed in the appropriate calendar system for the user's locale. The calendar system can be overridden using the Unicode calendar locale extension, passed to the I18nProvider component. Selected dates passed to `onChange` always use the same calendar system as the `value` or `defaultValue` prop. If no `value` or `defaultValue` is provided, then dates passed to `onChange` are always in the Gregorian calendar since this is the most commonly used. This means that even though the user selects dates in their local calendar system, applications are able to deal with dates from all users consistently. The below example displays a `DateField` in the Hindi language, using the Indian calendar. Dates emitted from `onChange` are in the Gregorian calendar. import {I18nProvider} from 'react-aria'; function Example() { let [date, setDate] = React.useState(null); return ( <I18nProvider locale="hi-IN-u-ca-indian"> <DateField label="Date" value={date} onChange={setDate} /> <p>Selected date: {date?.toString()}</p> </I18nProvider> ); } import {I18nProvider} from 'react-aria'; function Example() { let [date, setDate] = React.useState(null); return ( <I18nProvider locale="hi-IN-u-ca-indian"> <DateField label="Date" value={date} onChange={setDate} /> <p>Selected date: {date?.toString()}</p> </I18nProvider> ); } import {I18nProvider} from 'react-aria'; function Example() { let [date, setDate] = React.useState(null); return ( <I18nProvider locale="hi-IN-u-ca-indian"> <DateField label="Date" value={date} onChange={setDate} /> <p> Selected date: {' '} {date ?.toString()} </p> </I18nProvider> ); } Date dd/mm/yyyy शक Selected date: ### Minimum and maximum values# The `minValue` and `maxValue` props can also be used to perform builtin validation. This marks the date field as invalid using ARIA if the user enters an invalid date. You should implement a visual indication that the date field is invalid as well. This example only accepts dates after today. import {today} from '@internationalized/date'; <DateField label="Appointment date" minValue={today(getLocalTimeZone())} defaultValue={parseDate('2022-02-03')} /> import {today} from '@internationalized/date'; <DateField label="Appointment date" minValue={today(getLocalTimeZone())} defaultValue={parseDate('2022-02-03')} /> import {today} from '@internationalized/date'; <DateField label="Appointment date" minValue={today( getLocalTimeZone() )} defaultValue={parseDate( '2022-02-03' )} /> Appointment date 2/3/2022🚫 ### Placeholder value# When no value is set, a placeholder is shown. The format of the placeholder is influenced by the `granularity` and `placeholderValue` props. `placeholderValue` also controls the default values of each segment when the user first interacts with them, e.g. using the up and down arrow keys. By default, the `placeholderValue` is the current date at midnight, but you can set it to a more appropriate value if needed. import {CalendarDate} from '@internationalized/date'; <DateField label="Birth date" placeholderValue={new CalendarDate(1980, 1, 1)} /> import {CalendarDate} from '@internationalized/date'; <DateField label="Birth date" placeholderValue={new CalendarDate(1980, 1, 1)} /> import {CalendarDate} from '@internationalized/date'; <DateField label="Birth date" placeholderValue={new CalendarDate( 1980, 1, 1 )} /> Birth date mm/dd/yyyy ### Hide time zone# When a `ZonedDateTime` object is provided as the value to `useDateField`, the time zone abbreviation is displayed by default. However, if this is displayed elsewhere or implicit based on the usecase, it can be hidden using the `hideTimeZone` option. <DateField label="Appointment time" defaultValue={parseZonedDateTime('2022-11-07T10:45[America/Los_Angeles]')} hideTimeZone /> <DateField label="Appointment time" defaultValue={parseZonedDateTime( '2022-11-07T10:45[America/Los_Angeles]' )} hideTimeZone /> <DateField label="Appointment time" defaultValue={parseZonedDateTime( '2022-11-07T10:45[America/Los_Angeles]' )} hideTimeZone /> Appointment time 11/7/2022, 10:45 AM ### Hour cycle# By default, `useDateField` displays times in either 12 or 24 hour hour format depending on the user's locale. However, this can be overridden using the `hourCycle` prop if needed for a specific usecase. This example forces `useDateField` to use 24-hour time, regardless of the locale. <DateField label="Appointment time" granularity="minute" hourCycle={24} /> <DateField label="Appointment time" granularity="minute" hourCycle={24} /> <DateField label="Appointment time" granularity="minute" hourCycle={24} /> Appointment time mm/dd/yyyy, ––:–– ## Advanced topics# * * * ### Reducing bundle size# In the example above, the `createCalendar` function from the @internationalized/date package is passed to the` useDateFieldState `hook. This function receives a calendar identifier string, and provides` Calendar `instances to React Stately, which are used to implement date manipulation. By default, this includes all calendar systems supported by `@internationalized/date`. However, if your application supports a more limited set of regions, or you know you will only be picking dates in a certain calendar system, you can reduce your bundle size by providing your own implementation of `createCalendar` that includes a subset of these `Calendar` implementations. For example, if your application only supports Gregorian dates, you could implement a `createCalendar` function like this: import {useLocale} from 'react-aria'; import {useDateFieldState} from 'react-stately'; import {GregorianCalendar} from '@internationalized/date'; function createCalendar(identifier) { switch (identifier) { case 'gregory': return new GregorianCalendar(); default: throw new Error(`Unsupported calendar ${identifier}`); } } export function DateField(props) { let { locale } = useLocale(); let state = useDateFieldState({ ...props, locale, createCalendar }); // ... } import {useLocale} from 'react-aria'; import {useDateFieldState} from 'react-stately'; import {GregorianCalendar} from '@internationalized/date'; function createCalendar(identifier) { switch (identifier) { case 'gregory': return new GregorianCalendar(); default: throw new Error(`Unsupported calendar ${identifier}`); } } export function DateField(props) { let { locale } = useLocale(); let state = useDateFieldState({ ...props, locale, createCalendar }); // ... } import {useLocale} from 'react-aria'; import {useDateFieldState} from 'react-stately'; import {GregorianCalendar} from '@internationalized/date'; function createCalendar( identifier ) { switch (identifier) { case 'gregory': return new GregorianCalendar(); default: throw new Error( `Unsupported calendar ${identifier}` ); } } export function DateField( props ) { let { locale } = useLocale(); let state = useDateFieldState({ ...props, locale, createCalendar }); // ... } This way, only `GregorianCalendar` is imported, and the other calendar implementations can be tree-shaken. See the Calendar documentation in `@internationalized/date` to learn more about the supported calendar systems, and a list of string identifiers. --- ## Page: https://react-spectrum.adobe.com/react-aria/useDatePicker.html # useDatePicker Provides the behavior and accessibility implementation for a date picker component. A date picker combines a DateField and a Calendar popover to allow users to enter or select a date and time value. <table><tbody><tr><th>install</th><td><code>yarn add react-aria</code></td></tr><tr><th>version</th><td>3.41.1</td></tr><tr><th>usage</th><td><code><span>import</span> {useDatePicker} <span>from</span> <span>'react-aria'</span></code></td></tr></tbody></table> View repository GitHub View package NPM ## API# * * * `useDatePicker<T extends DateValue >( props: AriaDatePickerProps <T>, state: DatePickerState , ref: RefObject <Element | | null> ): DatePickerAria ` ## Features# * * * A date picker can be built using `<input type="date">`, but this is very limited in functionality, lacking in internationalization capabilities, inconsistent between browsers, and difficult to style. `useDatePicker` helps achieve accessible and international date and time pickers that can be styled as needed. * **Dates and times** – Support for dates and times with configurable granularity. * **International** – Support for 13 calendar systems used around the world, including Gregorian, Buddhist, Islamic, Persian, and more. Locale-specific formatting, number systems, hour cycles, and right-to-left support are available as well. * **Time zone aware** – Dates and times can optionally include a time zone. All modifications follow time zone rules such as daylight saving time. * **Accessible** – Each date and time unit is displayed as an individually focusable and editable segment, which allows users an easy way to edit dates using the keyboard, in any date format and locale. Users can also open a calendar popover to select dates in a standard month grid. Localized screen reader messages are included to announce when the selection and visible date range change. * **Touch friendly** – Date segments are editable using an easy to use numeric keypad, and all interactions are accessible using touch-based screen readers. * **Customizable** – As with all of React Aria, the DOM structure and styling of all elements can be fully customized. Read our blog post for more details about the internationalization, accessibility, and user experience features implemented by `useDatePicker`. ## Anatomy# * * * A date picker consists of a label, and group containing a date field and a button. Clicking the button opens a popup containing a calendar. The date field includes segments representing each unit of a date and time (e.g. years, months, days, etc.), each of which is individually focusable and editable using the keyboard. The calendar popup offers a more visual way of choosing a date. `useDatePicker` also supports optional description and error message elements, which can be used to provide more context about the field, and any validation messages. These are linked with the input via the `aria-describedby` attribute. `useDatePicker` returns props that you should spread onto the appropriate elements: | Name | Type | Description | | --- | --- | --- | | `labelProps` | `DOMAttributes` | Props for the date picker's visible label element, if any. | | `groupProps` | ` GroupDOMAttributes ` | Props for the grouping element containing the date field and button. | | `fieldProps` | ` AriaDatePickerProps < DateValue >` | Props for the date field. | | `buttonProps` | ` AriaButtonProps ` | Props for the popover trigger button. | | `descriptionProps` | `DOMAttributes` | Props for the description element, if any. | | `errorMessageProps` | `DOMAttributes` | Props for the error message element, if any. | | `dialogProps` | ` AriaDialogProps ` | Props for the popover dialog. | | `calendarProps` | ` CalendarProps < DateValue >` | Props for the calendar within the popover dialog. | | `isInvalid` | `boolean` | Whether the input value is invalid. | | `validationErrors` | `string[]` | The current error messages for the input if it is invalid, otherwise an empty array. | | `validationDetails` | `ValidityState` | The native validation details for the input. | State is managed by the `useDatePickerState` hook from `@react-stately/datepicker`. The state object should be passed as an argument to `useDatePicker`. If the date picker does not have a visible label, an `aria-label` or `aria-labelledby` prop must be passed instead to identify it to assistive technology. ## Date and time values# * * * Dates and times are represented in many different ways by cultures around the world. This includes differences in calendar systems, time zones, daylight saving time rules, date and time formatting, weekday and weekend rules, and much more. When building applications that support users around the world, it is important to handle these aspects correctly for each locale. `useDatePicker` uses the @internationalized/date library to represent dates and times. This package provides a library of objects and functions to perform date and time related manipulation, queries, and conversions that work across locales and calendars. Date and time objects can be converted to and from native JavaScript `Date` objects or ISO 8601 strings. See the documentation, or the examples below for more details. ## Example# * * * A `DatePicker` composes several other components to produce a composite element that can be used to enter dates with a keyboard, or select them on a calendar. The `DateField`, `Popover`, `Calendar`, and `Button` components used in this example are independent and can be used separately from the `DatePicker`. The code is available below, and documentation is available on the corresponding pages. import {useDatePicker} from 'react-aria'; import {useDatePickerState} from 'react-stately'; // Reuse the DateField, Popover, Dialog, Calendar, and Button from your component library. import {Button, Calendar, DateField, Dialog, Popover} from 'your-component-library'; function DatePicker(props) { let state = useDatePickerState(props); let ref = React.useRef(null); let { groupProps, labelProps, fieldProps, buttonProps, dialogProps, calendarProps } = useDatePicker(props, state, ref); return ( <div style={{ display: 'inline-flex', flexDirection: 'column' }}> <div {...labelProps}>{props.label}</div> <div {...groupProps} ref={ref} style={{ display: 'flex' }}> <DateField {...fieldProps} /> <Button {...buttonProps}>🗓</Button> </div> {state.isOpen && ( <Popover state={state} triggerRef={ref} placement="bottom start"> <Dialog {...dialogProps}> <Calendar {...calendarProps} firstDayOfWeek={props.firstDayOfWeek} /> </Dialog> </Popover> )} </div> ); } <DatePicker label="Event date" /> import {useDatePicker} from 'react-aria'; import {useDatePickerState} from 'react-stately'; // Reuse the DateField, Popover, Dialog, Calendar, and Button from your component library. import { Button, Calendar, DateField, Dialog, Popover } from 'your-component-library'; function DatePicker(props) { let state = useDatePickerState(props); let ref = React.useRef(null); let { groupProps, labelProps, fieldProps, buttonProps, dialogProps, calendarProps } = useDatePicker(props, state, ref); return ( <div style={{ display: 'inline-flex', flexDirection: 'column' }} > <div {...labelProps}>{props.label}</div> <div {...groupProps} ref={ref} style={{ display: 'flex' }} > <DateField {...fieldProps} /> <Button {...buttonProps}>🗓</Button> </div> {state.isOpen && ( <Popover state={state} triggerRef={ref} placement="bottom start" > <Dialog {...dialogProps}> <Calendar {...calendarProps} firstDayOfWeek={props.firstDayOfWeek} /> </Dialog> </Popover> )} </div> ); } <DatePicker label="Event date" /> import {useDatePicker} from 'react-aria'; import {useDatePickerState} from 'react-stately'; // Reuse the DateField, Popover, Dialog, Calendar, and Button from your component library. import { Button, Calendar, DateField, Dialog, Popover } from 'your-component-library'; function DatePicker( props ) { let state = useDatePickerState( props ); let ref = React.useRef( null ); let { groupProps, labelProps, fieldProps, buttonProps, dialogProps, calendarProps } = useDatePicker( props, state, ref ); return ( <div style={{ display: 'inline-flex', flexDirection: 'column' }} > <div {...labelProps} > {props.label} </div> <div {...groupProps} ref={ref} style={{ display: 'flex' }} > <DateField {...fieldProps} /> <Button {...buttonProps} > 🗓 </Button> </div> {state.isOpen && ( <Popover state={state} triggerRef={ref} placement="bottom start" > <Dialog {...dialogProps} > <Calendar {...calendarProps} firstDayOfWeek={props .firstDayOfWeek} /> </Dialog> </Popover> )} </div> ); } <DatePicker label="Event date" /> Event date mm / dd / yyyy 🗓 ### Button# The `Button` component is used in the above example to trigger the calendar popover. It is built using the useButton hook, and can be shared with many other components. Show code import {useButton} from 'react-aria'; function Button(props) { let ref = React.useRef(null); let { buttonProps } = useButton(props, ref); return <button {...buttonProps} ref={ref}>{props.children}</button>; } import {useButton} from 'react-aria'; function Button(props) { let ref = React.useRef(null); let { buttonProps } = useButton(props, ref); return ( <button {...buttonProps} ref={ref}> {props.children} </button> ); } import {useButton} from 'react-aria'; function Button(props) { let ref = React.useRef( null ); let { buttonProps } = useButton( props, ref ); return ( <button {...buttonProps} ref={ref} > {props.children} </button> ); } ### DateField# The `DateField` component implements the keyboard editable input used in a `DatePicker`. It can also be used standalone, or within a date range picker. See useDateField for more examples and documentation. Show code import {useDateFieldState} from 'react-stately'; import {useDateField, useDateSegment, useLocale} from 'react-aria'; function DateField(props) { let { locale } = useLocale(); let state = useDateFieldState({ ...props, locale, createCalendar }); let ref = React.useRef(null); let { labelProps, fieldProps } = useDateField(props, state, ref); return ( <div className="wrapper"> <span {...labelProps}>{props.label}</span> <div {...fieldProps} ref={ref} className="field"> {state.segments.map((segment, i) => ( <DateSegment key={i} segment={segment} state={state} /> ))} {state.isInvalid && <span aria-hidden="true">🚫</span>} </div> </div> ); } function DateSegment({ segment, state }) { let ref = React.useRef(null); let { segmentProps } = useDateSegment(segment, state, ref); return ( <div {...segmentProps} ref={ref} className={`segment ${segment.isPlaceholder ? 'placeholder' : ''}`} > {segment.text} </div> ); } import {useDateFieldState} from 'react-stately'; import { useDateField, useDateSegment, useLocale } from 'react-aria'; function DateField(props) { let { locale } = useLocale(); let state = useDateFieldState({ ...props, locale, createCalendar }); let ref = React.useRef(null); let { labelProps, fieldProps } = useDateField( props, state, ref ); return ( <div className="wrapper"> <span {...labelProps}>{props.label}</span> <div {...fieldProps} ref={ref} className="field"> {state.segments.map((segment, i) => ( <DateSegment key={i} segment={segment} state={state} /> ))} {state.isInvalid && <span aria-hidden="true">🚫</span>} </div> </div> ); } function DateSegment({ segment, state }) { let ref = React.useRef(null); let { segmentProps } = useDateSegment( segment, state, ref ); return ( <div {...segmentProps} ref={ref} className={`segment ${ segment.isPlaceholder ? 'placeholder' : '' }`} > {segment.text} </div> ); } import {useDateFieldState} from 'react-stately'; import { useDateField, useDateSegment, useLocale } from 'react-aria'; function DateField( props ) { let { locale } = useLocale(); let state = useDateFieldState({ ...props, locale, createCalendar }); let ref = React.useRef( null ); let { labelProps, fieldProps } = useDateField( props, state, ref ); return ( <div className="wrapper"> <span {...labelProps} > {props.label} </span> <div {...fieldProps} ref={ref} className="field" > {state.segments .map(( segment, i ) => ( <DateSegment key={i} segment={segment} state={state} /> ))} {state .isInvalid && ( <span aria-hidden="true"> 🚫 </span> )} </div> </div> ); } function DateSegment( { segment, state } ) { let ref = React.useRef( null ); let { segmentProps } = useDateSegment( segment, state, ref ); return ( <div {...segmentProps} ref={ref} className={`segment ${ segment .isPlaceholder ? 'placeholder' : '' }`} > {segment.text} </div> ); } Show CSS .wrapper { display: flex; flex-direction: column; align-items: flex-start; } .field { display: inline-flex; padding: 2px 4px; border-radius: 2px; border: 1px solid var(--gray); background: var(--spectrum-global-color-gray-50); } .field:focus-within { border-color: var(--blue); } .segment { padding: 0 2px; font-variant-numeric: tabular-nums; text-align: end; } .segment.placeholder { color: var(--spectrum-gray-600); } .segment:focus { color: white; background: var(--blue); outline: none; border-radius: 2px; } .wrapper { display: flex; flex-direction: column; align-items: flex-start; } .field { display: inline-flex; padding: 2px 4px; border-radius: 2px; border: 1px solid var(--gray); background: var(--spectrum-global-color-gray-50); } .field:focus-within { border-color: var(--blue); } .segment { padding: 0 2px; font-variant-numeric: tabular-nums; text-align: end; } .segment.placeholder { color: var(--spectrum-gray-600); } .segment:focus { color: white; background: var(--blue); outline: none; border-radius: 2px; } .wrapper { display: flex; flex-direction: column; align-items: flex-start; } .field { display: inline-flex; padding: 2px 4px; border-radius: 2px; border: 1px solid var(--gray); background: var(--spectrum-global-color-gray-50); } .field:focus-within { border-color: var(--blue); } .segment { padding: 0 2px; font-variant-numeric: tabular-nums; text-align: end; } .segment.placeholder { color: var(--spectrum-gray-600); } .segment:focus { color: white; background: var(--blue); outline: none; border-radius: 2px; } ### Popover# The `Popover` component is used to contain the popup calendar for the `DatePicker`. It can be shared between many other components, including Select, Menu, and others. See usePopover for more examples of popovers. Show code import {DismissButton, Overlay, usePopover} from 'react-aria'; import type {AriaPopoverProps} from 'react-aria'; import type {OverlayTriggerState} from 'react-stately'; interface PopoverProps extends Omit<AriaPopoverProps, 'popoverRef'> { children: React.ReactNode; state: OverlayTriggerState; } function Popover({ children, state, ...props }: PopoverProps) { let popoverRef = React.useRef(null); let { popoverProps, underlayProps } = usePopover({ ...props, popoverRef }, state); return ( <Overlay> <div {...underlayProps} style={{ position: 'fixed', inset: 0 }} /> <div {...popoverProps} ref={popoverRef} style={{ ...popoverProps.style, background: 'var(--page-background)', border: '1px solid gray' }} > <DismissButton onDismiss={state.close} /> {children} <DismissButton onDismiss={state.close} /> </div> </Overlay> ); } import { DismissButton, Overlay, usePopover } from 'react-aria'; import type {AriaPopoverProps} from 'react-aria'; import type {OverlayTriggerState} from 'react-stately'; interface PopoverProps extends Omit<AriaPopoverProps, 'popoverRef'> { children: React.ReactNode; state: OverlayTriggerState; } function Popover( { children, state, ...props }: PopoverProps ) { let popoverRef = React.useRef(null); let { popoverProps, underlayProps } = usePopover({ ...props, popoverRef }, state); return ( <Overlay> <div {...underlayProps} style={{ position: 'fixed', inset: 0 }} /> <div {...popoverProps} ref={popoverRef} style={{ ...popoverProps.style, background: 'var(--page-background)', border: '1px solid gray' }} > <DismissButton onDismiss={state.close} /> {children} <DismissButton onDismiss={state.close} /> </div> </Overlay> ); } import { DismissButton, Overlay, usePopover } from 'react-aria'; import type {AriaPopoverProps} from 'react-aria'; import type {OverlayTriggerState} from 'react-stately'; interface PopoverProps extends Omit< AriaPopoverProps, 'popoverRef' > { children: React.ReactNode; state: OverlayTriggerState; } function Popover( { children, state, ...props }: PopoverProps ) { let popoverRef = React .useRef(null); let { popoverProps, underlayProps } = usePopover({ ...props, popoverRef }, state); return ( <Overlay> <div {...underlayProps} style={{ position: 'fixed', inset: 0 }} /> <div {...popoverProps} ref={popoverRef} style={{ ...popoverProps .style, background: 'var(--page-background)', border: '1px solid gray' }} > <DismissButton onDismiss={state .close} /> {children} <DismissButton onDismiss={state .close} /> </div> </Overlay> ); } ### Dialog# The `Dialog` component is rendered within the `Popover` component. It is built using the useDialog hook, and can be shared with many other components. Show code import type {AriaDialogProps} from 'react-aria'; import {useDialog} from 'react-aria'; interface DialogProps extends AriaDialogProps { title?: React.ReactNode; children: React.ReactNode; } function Dialog({ title, children, ...props }: DialogProps) { let ref = React.useRef(null); let { dialogProps, titleProps } = useDialog(props, ref); return ( <div {...dialogProps} ref={ref} style={{ padding: 30 }}> {title && ( <h3 {...titleProps} style={{ marginTop: 0 }}> {title} </h3> )} {children} </div> ); } import type {AriaDialogProps} from 'react-aria'; import {useDialog} from 'react-aria'; interface DialogProps extends AriaDialogProps { title?: React.ReactNode; children: React.ReactNode; } function Dialog( { title, children, ...props }: DialogProps ) { let ref = React.useRef(null); let { dialogProps, titleProps } = useDialog(props, ref); return ( <div {...dialogProps} ref={ref} style={{ padding: 30 }}> {title && ( <h3 {...titleProps} style={{ marginTop: 0 }}> {title} </h3> )} {children} </div> ); } import type {AriaDialogProps} from 'react-aria'; import {useDialog} from 'react-aria'; interface DialogProps extends AriaDialogProps { title?: React.ReactNode; children: React.ReactNode; } function Dialog( { title, children, ...props }: DialogProps ) { let ref = React.useRef( null ); let { dialogProps, titleProps } = useDialog( props, ref ); return ( <div {...dialogProps} ref={ref} style={{ padding: 30 }} > {title && ( <h3 {...titleProps} style={{ marginTop: 0 }} > {title} </h3> )} {children} </div> ); } ### Calendar# The `Calendar` component implements the month grid shown within the `DatePicker` popover. It can also be used standalone, or within other components. See useCalendar for more examples and documentation. Show code import {useCalendar, useCalendarCell, useCalendarGrid} from 'react-aria'; import {useCalendarState} from 'react-stately'; import {createCalendar, getWeeksInMonth} from '@internationalized/date'; function Calendar(props) { let { locale } = useLocale(); let state = useCalendarState({ ...props, locale, createCalendar }); let { calendarProps, prevButtonProps, nextButtonProps, title } = useCalendar( props, state ); return ( <div {...calendarProps} className="calendar"> <div className="header"> <h2>{title}</h2> <Button {...prevButtonProps}><</Button> <Button {...nextButtonProps}>></Button> </div> <CalendarGrid state={state} firstDayOfWeek={props.firstDayOfWeek} /> </div> ); } function CalendarGrid({ state, ...props }) { let { locale } = useLocale(); let { gridProps, headerProps, weekDays } = useCalendarGrid(props, state); // Get the number of weeks in the month so we can render the proper number of rows. let weeksInMonth = getWeeksInMonth(state.visibleRange.start, locale); return ( <table {...gridProps}> <thead {...headerProps}> <tr> {weekDays.map((day, index) => <th key={index}>{day}</th>)} </tr> </thead> <tbody> {[...new Array(weeksInMonth).keys()].map((weekIndex) => ( <tr key={weekIndex}> {state.getDatesInWeek(weekIndex).map((date, i) => ( date ? ( <CalendarCell key={i} state={state} date={date} /> ) : <td key={i} /> ))} </tr> ))} </tbody> </table> ); } function CalendarCell({ state, date }) { let ref = React.useRef(null); let { cellProps, buttonProps, isSelected, isOutsideVisibleRange, isDisabled, isUnavailable, formattedDate } = useCalendarCell({ date }, state, ref); return ( <td {...cellProps}> <div {...buttonProps} ref={ref} hidden={isOutsideVisibleRange} className={`cell ${isSelected ? 'selected' : ''} ${ isDisabled ? 'disabled' : '' } ${isUnavailable ? 'unavailable' : ''}`} > {formattedDate} </div> </td> ); } import { useCalendar, useCalendarCell, useCalendarGrid } from 'react-aria'; import {useCalendarState} from 'react-stately'; import { createCalendar, getWeeksInMonth } from '@internationalized/date'; function Calendar(props) { let { locale } = useLocale(); let state = useCalendarState({ ...props, locale, createCalendar }); let { calendarProps, prevButtonProps, nextButtonProps, title } = useCalendar(props, state); return ( <div {...calendarProps} className="calendar"> <div className="header"> <h2>{title}</h2> <Button {...prevButtonProps}><</Button> <Button {...nextButtonProps}>></Button> </div> <CalendarGrid state={state} firstDayOfWeek={props.firstDayOfWeek} /> </div> ); } function CalendarGrid({ state, ...props }) { let { locale } = useLocale(); let { gridProps, headerProps, weekDays } = useCalendarGrid(props, state); // Get the number of weeks in the month so we can render the proper number of rows. let weeksInMonth = getWeeksInMonth( state.visibleRange.start, locale ); return ( <table {...gridProps}> <thead {...headerProps}> <tr> {weekDays.map((day, index) => ( <th key={index}>{day}</th> ))} </tr> </thead> <tbody> {[...new Array(weeksInMonth).keys()].map( (weekIndex) => ( <tr key={weekIndex}> {state.getDatesInWeek(weekIndex).map(( date, i ) => ( date ? ( <CalendarCell key={i} state={state} date={date} /> ) : <td key={i} /> ))} </tr> ) )} </tbody> </table> ); } function CalendarCell({ state, date }) { let ref = React.useRef(null); let { cellProps, buttonProps, isSelected, isOutsideVisibleRange, isDisabled, isUnavailable, formattedDate } = useCalendarCell({ date }, state, ref); return ( <td {...cellProps}> <div {...buttonProps} ref={ref} hidden={isOutsideVisibleRange} className={`cell ${isSelected ? 'selected' : ''} ${ isDisabled ? 'disabled' : '' } ${isUnavailable ? 'unavailable' : ''}`} > {formattedDate} </div> </td> ); } import { useCalendar, useCalendarCell, useCalendarGrid } from 'react-aria'; import {useCalendarState} from 'react-stately'; import { createCalendar, getWeeksInMonth } from '@internationalized/date'; function Calendar( props ) { let { locale } = useLocale(); let state = useCalendarState({ ...props, locale, createCalendar }); let { calendarProps, prevButtonProps, nextButtonProps, title } = useCalendar( props, state ); return ( <div {...calendarProps} className="calendar" > <div className="header"> <h2>{title}</h2> <Button {...prevButtonProps} > < </Button> <Button {...nextButtonProps} > > </Button> </div> <CalendarGrid state={state} firstDayOfWeek={props .firstDayOfWeek} /> </div> ); } function CalendarGrid( { state, ...props } ) { let { locale } = useLocale(); let { gridProps, headerProps, weekDays } = useCalendarGrid( props, state ); // Get the number of weeks in the month so we can render the proper number of rows. let weeksInMonth = getWeeksInMonth( state.visibleRange .start, locale ); return ( <table {...gridProps} > <thead {...headerProps} > <tr> {weekDays.map(( day, index ) => ( <th key={index} > {day} </th> ))} </tr> </thead> <tbody> {[...new Array( weeksInMonth ).keys()].map( (weekIndex) => ( <tr key={weekIndex} > {state .getDatesInWeek( weekIndex ).map(( date, i ) => ( date ? ( <CalendarCell key={i} state={state} date={date} /> ) : ( <td key={i} /> ) ))} </tr> ) )} </tbody> </table> ); } function CalendarCell( { state, date } ) { let ref = React.useRef( null ); let { cellProps, buttonProps, isSelected, isOutsideVisibleRange, isDisabled, isUnavailable, formattedDate } = useCalendarCell( { date }, state, ref ); return ( <td {...cellProps}> <div {...buttonProps} ref={ref} hidden={isOutsideVisibleRange} className={`cell ${ isSelected ? 'selected' : '' } ${ isDisabled ? 'disabled' : '' } ${ isUnavailable ? 'unavailable' : '' }`} > {formattedDate} </div> </td> ); } Show CSS .calendar { width: 220px; } .header { display: flex; align-items: center; gap: 4px; margin: 0 8px; } .header h2 { flex: 1; margin: 0; } .calendar table { width: 100%; } .cell { cursor: default; text-align: center; } .selected { background: var(--blue); color: white; } .unavailable { color: var(--spectrum-global-color-red-600); } .disabled { color: gray; } .calendar { width: 220px; } .header { display: flex; align-items: center; gap: 4px; margin: 0 8px; } .header h2 { flex: 1; margin: 0; } .calendar table { width: 100%; } .cell { cursor: default; text-align: center; } .selected { background: var(--blue); color: white; } .unavailable { color: var(--spectrum-global-color-red-600); } .disabled { color: gray; } .calendar { width: 220px; } .header { display: flex; align-items: center; gap: 4px; margin: 0 8px; } .header h2 { flex: 1; margin: 0; } .calendar table { width: 100%; } .cell { cursor: default; text-align: center; } .selected { background: var(--blue); color: white; } .unavailable { color: var(--spectrum-global-color-red-600); } .disabled { color: gray; } ## Styled examples# * * * Tailwind CSS A date picker built with Tailwind and React Aria. Chakra UI A date and time picker built with Chakra UI and React Aria. ## Usage# * * * The following examples show how to use the `DatePicker` component created in the above example. ### Value# A `DatePicker` displays a placeholder by default. An initial, uncontrolled value can be provided to the `DatePicker` using the `defaultValue` prop. Alternatively, a controlled value can be provided using the `value` prop. Date values are provided using objects in the @internationalized/date package. This library handles correct international date manipulation across calendars, time zones, and other localization concerns. `useDatePicker` supports values of the following types: * ` CalendarDate `– a date without any time components. May be parsed from a string representation using the` parseDate `function. Use this type to represent dates where the time is not important, such as a birthday or an all day calendar event. * ` CalendarDateTime `– a date with a time, but not in any specific time zone. May be parsed from a string representation using the` parseDateTime `function. Use this type to represent times that occur at the same local time regardless of the time zone, such as the time of New Years Eve fireworks which always occur at midnight. Most times are better stored as a `ZonedDateTime`. * ` ZonedDateTime `– a date with a time in a specific time zone. May be parsed from a string representation using the` parseZonedDateTime `,` parseAbsolute `, or` parseAbsoluteToLocal `functions. Use this type to represent an exact moment in time at a particular location on Earth. import {parseDate} from '@internationalized/date'; function Example() { let [value, setValue] = React.useState(parseDate('2020-02-03')); return ( <> <DatePicker label="Date (uncontrolled)" defaultValue={parseDate('2020-02-03')} /> <DatePicker label="Date (controlled)" value={value} onChange={setValue} /> </> ); } import {parseDate} from '@internationalized/date'; function Example() { let [value, setValue] = React.useState( parseDate('2020-02-03') ); return ( <> <DatePicker label="Date (uncontrolled)" defaultValue={parseDate('2020-02-03')} /> <DatePicker label="Date (controlled)" value={value} onChange={setValue} /> </> ); } import {parseDate} from '@internationalized/date'; function Example() { let [value, setValue] = React.useState( parseDate( '2020-02-03' ) ); return ( <> <DatePicker label="Date (uncontrolled)" defaultValue={parseDate( '2020-02-03' )} /> <DatePicker label="Date (controlled)" value={value} onChange={setValue} /> </> ); } Date (uncontrolled) 2 / 3 / 2020 🗓 Date (controlled) 2 / 3 / 2020 🗓 ### Events# `useDatePicker` accepts an `onChange` prop which is triggered whenever the date is edited by the user. The example below uses `onChange` to update a separate element with a formatted version of the date in the user's locale and local time zone. This is done by converting the date to a native JavaScript `Date` object to pass to the formatter. import {useDateFormatter} from 'react-aria'; import {getLocalTimeZone} from '@internationalized/date'; function Example() { let [date, setDate] = React.useState(parseDate('1985-07-03')); let formatter = useDateFormatter({ dateStyle: 'full' }); return ( <> <DatePicker label="Birth date" value={date} onChange={setDate} /> <p> Selected date:{' '} {date ? formatter.format(date.toDate(getLocalTimeZone())) : '--'} </p> </> ); } import {useDateFormatter} from 'react-aria'; import {getLocalTimeZone} from '@internationalized/date'; function Example() { let [date, setDate] = React.useState( parseDate('1985-07-03') ); let formatter = useDateFormatter({ dateStyle: 'full' }); return ( <> <DatePicker label="Birth date" value={date} onChange={setDate} /> <p> Selected date: {date ? formatter.format( date.toDate(getLocalTimeZone()) ) : '--'} </p> </> ); } import {useDateFormatter} from 'react-aria'; import {getLocalTimeZone} from '@internationalized/date'; function Example() { let [date, setDate] = React.useState( parseDate( '1985-07-03' ) ); let formatter = useDateFormatter({ dateStyle: 'full' }); return ( <> <DatePicker label="Birth date" value={date} onChange={setDate} /> <p> Selected date: {' '} {date ? formatter .format( date .toDate( getLocalTimeZone() ) ) : '--'} </p> </> ); } Birth date 7 / 3 / 1985 🗓 Selected date: Wednesday, July 3, 1985 ### Time zones# `useDatePicker` is time zone aware when a `ZonedDateTime` object is provided as the value. In this case, the time zone abbreviation is displayed, and time zone concerns such as daylight saving time are taken into account when the value is manipulated. In most cases, your data will come from and be sent to a server as an ISO 8601 formatted string. @internationalized/date includes functions for parsing strings in multiple formats into `ZonedDateTime` objects. Which format you use will depend on what information you need to store. * ` parseZonedDateTime `– This function parses a date with an explicit time zone and optional UTC offset attached (e.g. `"2021-11-07T00:45[America/Los_Angeles]"` or `"2021-11-07T00:45-07:00[America/Los_Angeles]"`). This format preserves the maximum amount of information. If the exact local time and time zone that a user selected is important, use this format. Storing the time zone and offset that was selected rather than converting to UTC ensures that the local time is correct regardless of daylight saving rule changes (e.g. if a locale abolishes DST). Examples where this applies include calendar events, reminders, and other times that occur in a particular location. * ` parseAbsolute `– This function parses an absolute date and time that occurs at the same instant at all locations on Earth. It can be represented in UTC (e.g. `"2021-11-07T07:45:00Z"`), or stored with a particular offset (e.g. `"2021-11-07T07:45:00-07:00"`). A time zone identifier, e.g. `America/Los_Angeles`, must be passed, and the result will be converted into that time zone. Absolute times are the best way to represent events that occurred in the past, or future events where an exact time is needed, regardless of time zone. * ` parseAbsoluteToLocal `– This function parses an absolute date and time into the current user's local time zone. It is a shortcut for `parseAbsolute`, and accepts the same formats. import {parseZonedDateTime} from '@internationalized/date'; <DatePicker label="Event date" defaultValue={parseZonedDateTime('2022-11-07T00:45[America/Los_Angeles]')} /> import {parseZonedDateTime} from '@internationalized/date'; <DatePicker label="Event date" defaultValue={parseZonedDateTime( '2022-11-07T00:45[America/Los_Angeles]' )} /> import {parseZonedDateTime} from '@internationalized/date'; <DatePicker label="Event date" defaultValue={parseZonedDateTime( '2022-11-07T00:45[America/Los_Angeles]' )} /> Event date 11 / 7 / 2022 , 12 : 45 AM PST 🗓 `useDatePicker` displays times in the time zone included in the `ZonedDateTime` object. The above example is always displayed in Pacific Standard Time because the `America/Los_Angeles` time zone identifier is provided. @internationalized/date includes functions for converting dates between time zones, or parsing a date directly into a specific time zone or the user's local time zone, as shown below. import {parseAbsoluteToLocal} from '@internationalized/date'; <DatePicker label="Event date" defaultValue={parseAbsoluteToLocal('2021-11-07T07:45:00Z')} /> import {parseAbsoluteToLocal} from '@internationalized/date'; <DatePicker label="Event date" defaultValue={parseAbsoluteToLocal( '2021-11-07T07:45:00Z' )} /> import {parseAbsoluteToLocal} from '@internationalized/date'; <DatePicker label="Event date" defaultValue={parseAbsoluteToLocal( '2021-11-07T07:45:00Z' )} /> Event date 11 / 7 / 2021 , 7 : 45 AM UTC 🗓 ### Granularity# The `granularity` prop allows you to control the smallest unit that is displayed by `useDatePicker`. By default, `CalendarDate` values are displayed with `"day"` granularity (year, month, and day), and `CalendarDateTime` and `ZonedDateTime` values are displayed with `"minute"` granularity. More granular time values can be displayed by setting the `granularity` prop to `"second"`. In addition, when a value with a time is provided but you wish to only display the date, you can set the granularity to `"day"`. This has no effect on the actual value (it still has a time component), only on what fields are displayed. In the following example, two DatePickers are synchronized with the same value, but display different granularities. function Example() { let [date, setDate] = React.useState( parseAbsoluteToLocal('2021-04-07T18:45:22Z') ); return ( <> <DatePicker label="Date and time" granularity="second" value={date} onChange={setDate} /> <DatePicker label="Date" granularity="day" value={date} onChange={setDate} /> </> ); } function Example() { let [date, setDate] = React.useState( parseAbsoluteToLocal('2021-04-07T18:45:22Z') ); return ( <> <DatePicker label="Date and time" granularity="second" value={date} onChange={setDate} /> <DatePicker label="Date" granularity="day" value={date} onChange={setDate} /> </> ); } function Example() { let [date, setDate] = React.useState( parseAbsoluteToLocal( '2021-04-07T18:45:22Z' ) ); return ( <> <DatePicker label="Date and time" granularity="second" value={date} onChange={setDate} /> <DatePicker label="Date" granularity="day" value={date} onChange={setDate} /> </> ); } Date and time 4 / 7 / 2021 , 6 : 45 : 22 PM UTC 🗓 Date 4 / 7 / 2021 🗓 If no `value` or `defaultValue` prop is passed, then the `granularity` prop also affects which type of value is emitted from the `onChange` event. Note that by default, time values will not have a time zone because none was supplied. You can override this by setting the `placeholderValue` prop explicitly. Values emitted from `onChange` will use the time zone of the placeholder value. import {now} from '@internationalized/date'; <DatePicker label="Event date" granularity="second" /> <DatePicker label="Event date" placeholderValue={now('America/New_York')} granularity="second" /> import {now} from '@internationalized/date'; <DatePicker label="Event date" granularity="second" /> <DatePicker label="Event date" placeholderValue={now('America/New_York')} granularity="second" /> import {now} from '@internationalized/date'; <DatePicker label="Event date" granularity="second" /> <DatePicker label="Event date" placeholderValue={now( 'America/New_York' )} granularity="second" /> Event date mm / dd / yyyy , –– : –– : –– AM 🗓 Event date mm / dd / yyyy , –– : –– : –– AM EDT 🗓 ### International calendars# `useDatePicker` supports selecting dates in many calendar systems used around the world, including Gregorian, Hebrew, Indian, Islamic, Buddhist, and more. Dates are automatically displayed in the appropriate calendar system for the user's locale. The calendar system can be overridden using the Unicode calendar locale extension, passed to the I18nProvider component. Selected dates passed to `onChange` always use the same calendar system as the `value` or `defaultValue` prop. If no `value` or `defaultValue` is provided, then dates passed to `onChange` are always in the Gregorian calendar since this is the most commonly used. This means that even though the user selects dates in their local calendar system, applications are able to deal with dates from all users consistently. The below example displays a `DatePicker` in the Hindi language, using the Indian calendar. Dates emitted from `onChange` are in the Gregorian calendar. import {I18nProvider} from 'react-aria'; function Example() { let [date, setDate] = React.useState(null); return ( <I18nProvider locale="hi-IN-u-ca-indian"> <DatePicker label="Date" value={date} onChange={setDate} /> <p>Selected date: {date?.toString()}</p> </I18nProvider> ); } import {I18nProvider} from 'react-aria'; function Example() { let [date, setDate] = React.useState(null); return ( <I18nProvider locale="hi-IN-u-ca-indian"> <DatePicker label="Date" value={date} onChange={setDate} /> <p>Selected date: {date?.toString()}</p> </I18nProvider> ); } import {I18nProvider} from 'react-aria'; function Example() { let [date, setDate] = React.useState(null); return ( <I18nProvider locale="hi-IN-u-ca-indian"> <DatePicker label="Date" value={date} onChange={setDate} /> <p> Selected date: {' '} {date ?.toString()} </p> </I18nProvider> ); } Date dd / mm / yyyy शक 🗓 Selected date: ### Minimum and maximum values# The `minValue` and `maxValue` props can also be used to perform builtin validation. This prevents the user from selecting dates outside the valid range in the calendar, and marks the date field as invalid using ARIA. You should implement a visual indication that the date picker is invalid as well. This example only accepts dates after today. import {today} from '@internationalized/date'; <DatePicker label="Appointment date" minValue={today(getLocalTimeZone())} defaultValue={parseDate('2022-02-03')} /> import {today} from '@internationalized/date'; <DatePicker label="Appointment date" minValue={today(getLocalTimeZone())} defaultValue={parseDate('2022-02-03')} /> import {today} from '@internationalized/date'; <DatePicker label="Appointment date" minValue={today( getLocalTimeZone() )} defaultValue={parseDate( '2022-02-03' )} /> Appointment date 2 / 3 / 2022 🚫 🗓 ### Unavailable dates# `useDatePicker` supports marking certain dates as _unavailable_. These dates remain focusable with the keyboard in the calendar so that navigation is consistent, but cannot be selected by the user. When an unavailable date is entered into the date field, it is marked as invalid. The `isDateUnavailable` prop accepts a callback that is called to evaluate whether each visible date is unavailable. This example includes multiple unavailable date ranges, e.g. dates when no appointments are available. In addition, all weekends are unavailable. The `minValue` prop is also used to prevent selecting dates before today. import {useLocale} from 'react-aria'; import {isWeekend, today} from '@internationalized/date'; function Example() { let now = today(getLocalTimeZone()); let disabledRanges = [ [now, now.add({ days: 5 })], [now.add({ days: 14 }), now.add({ days: 16 })], [now.add({ days: 23 }), now.add({ days: 24 })] ]; let { locale } = useLocale(); let isDateUnavailable = (date) => isWeekend(date, locale) || disabledRanges.some((interval) => date.compare(interval[0]) >= 0 && date.compare(interval[1]) <= 0 ); return ( <DatePicker label="Appointment date" minValue={today(getLocalTimeZone())} isDateUnavailable={isDateUnavailable} /> ); } import {useLocale} from 'react-aria'; import {isWeekend, today} from '@internationalized/date'; function Example() { let now = today(getLocalTimeZone()); let disabledRanges = [ [now, now.add({ days: 5 })], [now.add({ days: 14 }), now.add({ days: 16 })], [now.add({ days: 23 }), now.add({ days: 24 })] ]; let { locale } = useLocale(); let isDateUnavailable = (date) => isWeekend(date, locale) || disabledRanges.some((interval) => date.compare(interval[0]) >= 0 && date.compare(interval[1]) <= 0 ); return ( <DatePicker label="Appointment date" minValue={today(getLocalTimeZone())} isDateUnavailable={isDateUnavailable} /> ); } import {useLocale} from 'react-aria'; import { isWeekend, today } from '@internationalized/date'; function Example() { let now = today( getLocalTimeZone() ); let disabledRanges = [ [ now, now.add({ days: 5 }) ], [ now.add({ days: 14 }), now.add({ days: 16 }) ], [ now.add({ days: 23 }), now.add({ days: 24 }) ] ]; let { locale } = useLocale(); let isDateUnavailable = (date) => isWeekend( date, locale ) || disabledRanges .some(( interval ) => date.compare( interval[0] ) >= 0 && date.compare( interval[1] ) <= 0 ); return ( <DatePicker label="Appointment date" minValue={today( getLocalTimeZone() )} isDateUnavailable={isDateUnavailable} /> ); } Appointment date mm / dd / yyyy 🗓 ### Placeholder value# When no value is set, a placeholder is shown. The format of the placeholder is influenced by the `granularity` and `placeholderValue` props. `placeholderValue` also controls the default values of each segment when the user first interacts with them, e.g. using the up and down arrow keys, as well as the default month shown in the calendar popover. By default, the `placeholderValue` is the current date at midnight, but you can set it to a more appropriate value if needed. import {CalendarDate} from '@internationalized/date'; <DatePicker label="Birth date" placeholderValue={new CalendarDate(1980, 1, 1)} /> import {CalendarDate} from '@internationalized/date'; <DatePicker label="Birth date" placeholderValue={new CalendarDate(1980, 1, 1)} /> import {CalendarDate} from '@internationalized/date'; <DatePicker label="Birth date" placeholderValue={new CalendarDate( 1980, 1, 1 )} /> Birth date mm / dd / yyyy 🗓 ### Hide time zone# When a `ZonedDateTime` object is provided as the value to `useDatePicker`, the time zone abbreviation is displayed by default. However, if this is displayed elsewhere or implicit based on the usecase, it can be hidden using the `hideTimeZone` option. <DatePicker label="Appointment time" defaultValue={parseZonedDateTime('2022-11-07T10:45[America/Los_Angeles]')} hideTimeZone /> <DatePicker label="Appointment time" defaultValue={parseZonedDateTime( '2022-11-07T10:45[America/Los_Angeles]' )} hideTimeZone /> <DatePicker label="Appointment time" defaultValue={parseZonedDateTime( '2022-11-07T10:45[America/Los_Angeles]' )} hideTimeZone /> Appointment time 11 / 7 / 2022 , 10 : 45 AM 🗓 ### Hour cycle# By default, `useDatePicker` displays times in either 12 or 24 hour hour format depending on the user's locale. However, this can be overridden using the `hourCycle` prop if needed for a specific usecase. This example forces `useDatePicker` to use 24-hour time, regardless of the locale. <DatePicker label="Appointment time" granularity="minute" hourCycle={24} /> <DatePicker label="Appointment time" granularity="minute" hourCycle={24} /> <DatePicker label="Appointment time" granularity="minute" hourCycle={24} /> Appointment time mm / dd / yyyy , –– : –– 🗓 ### Custom first day of week# By default, the first day of the week is automatically set based on the current locale. This can be changed by setting the `firstDayOfWeek` prop to `'sun'`, `'mon'`, `'tue'`, `'wed'`, `'thu'`, `'fri'`, or `'sat'`. <DatePicker label="Appointment time" firstDayOfWeek="mon" /> <DatePicker label="Appointment time" firstDayOfWeek="mon" /> <DatePicker label="Appointment time" firstDayOfWeek="mon" /> Appointment time mm / dd / yyyy 🗓 --- ## Page: https://react-spectrum.adobe.com/react-aria/useDateRangePicker.html # useDateRangePicker Provides the behavior and accessibility implementation for a date picker component. A date range picker combines two DateFields and a RangeCalendar popover to allow users to enter or select a date and time range. <table><tbody><tr><th>install</th><td><code>yarn add react-aria</code></td></tr><tr><th>version</th><td>3.41.1</td></tr><tr><th>usage</th><td><code><span>import</span> {useDateRangePicker} <span>from</span> <span>'react-aria'</span></code></td></tr></tbody></table> View repository GitHub View package NPM ## API# * * * `useDateRangePicker<T extends DateValue >( props: AriaDateRangePickerProps <T>, state: DateRangePickerState , ref: RefObject <Element | | null> ): DateRangePickerAria ` ## Features# * * * A date range picker can be built using two separate `<input type="date">` elements, but this is very limited in functionality, lacking in internationalization capabilities, inconsistent between browsers, and difficult to style. `useDateRangePicker` helps achieve accessible and international date and time range pickers that can be styled as needed. * **Dates and times** – Support for dates and times with configurable granularity. * **International** – Support for 13 calendar systems used around the world, including Gregorian, Buddhist, Islamic, Persian, and more. Locale-specific formatting, number systems, hour cycles, and right-to-left support are available as well. * **Time zone aware** – Dates and times can optionally include a time zone. All modifications follow time zone rules such as daylight saving time. * **Accessible** – Each date and time unit is displayed as an individually focusable and editable segment, which allows users an easy way to edit dates using the keyboard, in any date format and locale. Users can also open a calendar popover to select date ranges in a standard month grid. Localized screen reader messages are included to announce when the selection and visible date range change. * **Touch friendly** – Date segments are editable using an easy to use numeric keypad, date ranges can be selected by dragging over dates in the calendar using a touch screen, and all interactions are accessible using touch-based screen readers. * **Customizable** – As with all of React Aria, the DOM structure and styling of all elements can be fully customized. Read our blog post for more details about the internationalization, accessibility, and user experience features implemented by `useDateRangePicker`. ## Anatomy# * * * A date range picker consists of a label, and group containing two date fields and a button. Clicking the button opens a popup containing a range calendar. The date fields include segments representing each unit of a date and time (e.g. years, months, days, etc.), each of which is individually focusable and editable using the keyboard. The calendar popup offers a more visual way of choosing a date range. `useDateRangePicker` also supports optional description and error message elements, which can be used to provide more context about the field, and any validation messages. These are linked with the input via the `aria-describedby` attribute. `useDateRangePicker` returns props that you should spread onto the appropriate elements: | Name | Type | Description | | --- | --- | --- | | `labelProps` | `DOMAttributes` | Props for the date range picker's visible label element, if any. | | `groupProps` | ` GroupDOMAttributes ` | Props for the grouping element containing the date fields and button. | | `startFieldProps` | ` AriaDatePickerProps < DateValue >` | Props for the start date field. | | `endFieldProps` | ` AriaDatePickerProps < DateValue >` | Props for the end date field. | | `buttonProps` | ` AriaButtonProps ` | Props for the popover trigger button. | | `descriptionProps` | `DOMAttributes` | Props for the description element, if any. | | `errorMessageProps` | `DOMAttributes` | Props for the error message element, if any. | | `dialogProps` | ` AriaDialogProps ` | Props for the popover dialog. | | `calendarProps` | ` RangeCalendarProps < DateValue >` | Props for the range calendar within the popover dialog. | | `isInvalid` | `boolean` | Whether the input value is invalid. | | `validationErrors` | `string[]` | The current error messages for the input if it is invalid, otherwise an empty array. | | `validationDetails` | `ValidityState` | The native validation details for the input. | State is managed by the `useDateRangePickerState` hook from `@react-stately/datepicker`. The state object should be passed as an argument to `useDateRangePicker`. If the date range picker does not have a visible label, an `aria-label` or `aria-labelledby` prop must be passed instead to identify it to assistive technology. ## Date and time values# * * * Dates and times are represented in many different ways by cultures around the world. This includes differences in calendar systems, time zones, daylight saving time rules, date and time formatting, weekday and weekend rules, and much more. When building applications that support users around the world, it is important to handle these aspects correctly for each locale. `useDateRangePicker` uses the @internationalized/date library to represent dates and times. This package provides a library of objects and functions to perform date and time related manipulation, queries, and conversions that work across locales and calendars. Date and time objects can be converted to and from native JavaScript `Date` objects or ISO 8601 strings. See the documentation, or the examples below for more details. ## Example# * * * A `DateRangePicker` composes several other components to produce a composite element that can be used to enter date ranges with a keyboard, or select them on a calendar. The `DateField`, `Popover`, `Calendar`, and `Button` components used in this example are independent and can be used separately from the `DateRangePicker`. The code is available below, and documentation is available on the corresponding pages. import {useDateRangePicker} from 'react-aria'; import {useDateRangePickerState} from 'react-stately'; // Reuse the DateField, Popover, Dialog, RangeCalendar, and Button from your component library. import {Button, DateField, Dialog, Popover, RangeCalendar} from 'your-component-library'; function DateRangePicker(props) { let state = useDateRangePickerState(props); let ref = React.useRef(null); let { labelProps, groupProps, startFieldProps, endFieldProps, buttonProps, dialogProps, calendarProps } = useDateRangePicker(props, state, ref); return ( <div style={{ display: 'inline-flex', flexDirection: 'column' }}> <span {...labelProps}>{props.label}</span> <div {...groupProps} ref={ref} style={{ display: 'flex' }}> <div className="field"> <DateField {...startFieldProps} /> <span style={{ padding: '0 4px' }}>–</span> <DateField {...endFieldProps} /> {state.isInvalid && <span aria-hidden="true">🚫</span>} </div> <Button {...buttonProps}>🗓</Button> </div> {state.isOpen && ( <Popover state={state} triggerRef={ref} placement="bottom start"> <Dialog {...dialogProps}> <RangeCalendar {...calendarProps} firstDayOfWeek={props.firstDayOfWeek} /> </Dialog> </Popover> )} </div> ); } <DateRangePicker label="Event date" /> import {useDateRangePicker} from 'react-aria'; import {useDateRangePickerState} from 'react-stately'; // Reuse the DateField, Popover, Dialog, RangeCalendar, and Button from your component library. import { Button, DateField, Dialog, Popover, RangeCalendar } from 'your-component-library'; function DateRangePicker(props) { let state = useDateRangePickerState(props); let ref = React.useRef(null); let { labelProps, groupProps, startFieldProps, endFieldProps, buttonProps, dialogProps, calendarProps } = useDateRangePicker(props, state, ref); return ( <div style={{ display: 'inline-flex', flexDirection: 'column' }} > <span {...labelProps}>{props.label}</span> <div {...groupProps} ref={ref} style={{ display: 'flex' }} > <div className="field"> <DateField {...startFieldProps} /> <span style={{ padding: '0 4px' }}>–</span> <DateField {...endFieldProps} /> {state.isInvalid && <span aria-hidden="true">🚫</span>} </div> <Button {...buttonProps}>🗓</Button> </div> {state.isOpen && ( <Popover state={state} triggerRef={ref} placement="bottom start" > <Dialog {...dialogProps}> <RangeCalendar {...calendarProps} firstDayOfWeek={props.firstDayOfWeek} /> </Dialog> </Popover> )} </div> ); } <DateRangePicker label="Event date" /> import {useDateRangePicker} from 'react-aria'; import {useDateRangePickerState} from 'react-stately'; // Reuse the DateField, Popover, Dialog, RangeCalendar, and Button from your component library. import { Button, DateField, Dialog, Popover, RangeCalendar } from 'your-component-library'; function DateRangePicker( props ) { let state = useDateRangePickerState( props ); let ref = React.useRef( null ); let { labelProps, groupProps, startFieldProps, endFieldProps, buttonProps, dialogProps, calendarProps } = useDateRangePicker( props, state, ref ); return ( <div style={{ display: 'inline-flex', flexDirection: 'column' }} > <span {...labelProps} > {props.label} </span> <div {...groupProps} ref={ref} style={{ display: 'flex' }} > <div className="field"> <DateField {...startFieldProps} /> <span style={{ padding: '0 4px' }} > – </span> <DateField {...endFieldProps} /> {state .isInvalid && ( <span aria-hidden="true"> 🚫 </span> )} </div> <Button {...buttonProps} > 🗓 </Button> </div> {state.isOpen && ( <Popover state={state} triggerRef={ref} placement="bottom start" > <Dialog {...dialogProps} > <RangeCalendar {...calendarProps} firstDayOfWeek={props .firstDayOfWeek} /> </Dialog> </Popover> )} </div> ); } <DateRangePicker label="Event date" /> Event date mm / dd / yyyy – mm / dd / yyyy 🗓 ### Button# The `Button` component is used in the above example to trigger the calendar popover. It is built using the useButton hook, and can be shared with many other components. Show code import {useButton} from 'react-aria'; function Button(props) { let ref = React.useRef(null); let { buttonProps } = useButton(props, ref); return <button {...buttonProps} ref={ref}>{props.children}</button>; } import {useButton} from 'react-aria'; function Button(props) { let ref = React.useRef(null); let { buttonProps } = useButton(props, ref); return ( <button {...buttonProps} ref={ref}> {props.children} </button> ); } import {useButton} from 'react-aria'; function Button(props) { let ref = React.useRef( null ); let { buttonProps } = useButton( props, ref ); return ( <button {...buttonProps} ref={ref} > {props.children} </button> ); } ### DateField# The `DateField` component implements the keyboard editable inputs used in a `DateRangePicker`. It can also be used standalone, or within a single date picker. See useDateField for more examples and documentation. Show code import {useDateFieldState} from 'react-stately'; import {useDateField, useDateSegment, useLocale} from 'react-aria'; function DateField(props) { let { locale } = useLocale(); let state = useDateFieldState({ ...props, locale, createCalendar }); let ref = React.useRef(null); let { labelProps, fieldProps } = useDateField(props, state, ref); return ( <div className="wrapper"> <span {...labelProps}>{props.label}</span> <div {...fieldProps} ref={ref} className="field"> {state.segments.map((segment, i) => ( <DateSegment key={i} segment={segment} state={state} /> ))} </div> </div> ); } function DateSegment({ segment, state }) { let ref = React.useRef(null); let { segmentProps } = useDateSegment(segment, state, ref); return ( <div {...segmentProps} ref={ref} className={`segment ${segment.isPlaceholder ? 'placeholder' : ''}`} > {segment.text} </div> ); } import {useDateFieldState} from 'react-stately'; import { useDateField, useDateSegment, useLocale } from 'react-aria'; function DateField(props) { let { locale } = useLocale(); let state = useDateFieldState({ ...props, locale, createCalendar }); let ref = React.useRef(null); let { labelProps, fieldProps } = useDateField( props, state, ref ); return ( <div className="wrapper"> <span {...labelProps}>{props.label}</span> <div {...fieldProps} ref={ref} className="field"> {state.segments.map((segment, i) => ( <DateSegment key={i} segment={segment} state={state} /> ))} </div> </div> ); } function DateSegment({ segment, state }) { let ref = React.useRef(null); let { segmentProps } = useDateSegment( segment, state, ref ); return ( <div {...segmentProps} ref={ref} className={`segment ${ segment.isPlaceholder ? 'placeholder' : '' }`} > {segment.text} </div> ); } import {useDateFieldState} from 'react-stately'; import { useDateField, useDateSegment, useLocale } from 'react-aria'; function DateField( props ) { let { locale } = useLocale(); let state = useDateFieldState({ ...props, locale, createCalendar }); let ref = React.useRef( null ); let { labelProps, fieldProps } = useDateField( props, state, ref ); return ( <div className="wrapper"> <span {...labelProps} > {props.label} </span> <div {...fieldProps} ref={ref} className="field" > {state.segments .map(( segment, i ) => ( <DateSegment key={i} segment={segment} state={state} /> ))} </div> </div> ); } function DateSegment( { segment, state } ) { let ref = React.useRef( null ); let { segmentProps } = useDateSegment( segment, state, ref ); return ( <div {...segmentProps} ref={ref} className={`segment ${ segment .isPlaceholder ? 'placeholder' : '' }`} > {segment.text} </div> ); } Show CSS .wrapper { display: flex; flex-direction: column; align-items: flex-start; } .field { display: inline-flex; padding: 2px 4px; border-radius: 2px; border: 1px solid var(--gray); background: var(--spectrum-global-color-gray-50); max-width: 100%; overflow: auto; } .field:focus-within { border-color: var(--blue); } .field .field { all: initial; display: inline-flex; color: inherit; } .segment { padding: 0 2px; font-variant-numeric: tabular-nums; text-align: end; } .segment.placeholder { color: var(--spectrum-gray-600); } .segment:focus { color: white; background: var(--blue); outline: none; border-radius: 2px; } .wrapper { display: flex; flex-direction: column; align-items: flex-start; } .field { display: inline-flex; padding: 2px 4px; border-radius: 2px; border: 1px solid var(--gray); background: var(--spectrum-global-color-gray-50); max-width: 100%; overflow: auto; } .field:focus-within { border-color: var(--blue); } .field .field { all: initial; display: inline-flex; color: inherit; } .segment { padding: 0 2px; font-variant-numeric: tabular-nums; text-align: end; } .segment.placeholder { color: var(--spectrum-gray-600); } .segment:focus { color: white; background: var(--blue); outline: none; border-radius: 2px; } .wrapper { display: flex; flex-direction: column; align-items: flex-start; } .field { display: inline-flex; padding: 2px 4px; border-radius: 2px; border: 1px solid var(--gray); background: var(--spectrum-global-color-gray-50); max-width: 100%; overflow: auto; } .field:focus-within { border-color: var(--blue); } .field .field { all: initial; display: inline-flex; color: inherit; } .segment { padding: 0 2px; font-variant-numeric: tabular-nums; text-align: end; } .segment.placeholder { color: var(--spectrum-gray-600); } .segment:focus { color: white; background: var(--blue); outline: none; border-radius: 2px; } ### Popover# The `Popover` component is used to contain the popup calendar for the `DateRangePicker`. It can be shared between many other components, including Select, Menu, and others. See usePopover for more examples of popovers. Show code import {DismissButton, Overlay, usePopover} from 'react-aria'; import type {AriaPopoverProps} from 'react-aria'; import type {OverlayTriggerState} from 'react-stately'; interface PopoverProps extends Omit<AriaPopoverProps, 'popoverRef'> { children: React.ReactNode; state: OverlayTriggerState; } function Popover({ children, state, ...props }: PopoverProps) { let popoverRef = React.useRef(null); let { popoverProps, underlayProps } = usePopover({ ...props, popoverRef }, state); return ( <Overlay> <div {...underlayProps} style={{ position: 'fixed', inset: 0 }} /> <div {...popoverProps} ref={popoverRef} style={{ ...popoverProps.style, background: 'var(--page-background)', border: '1px solid gray' }} > <DismissButton onDismiss={state.close} /> {children} <DismissButton onDismiss={state.close} /> </div> </Overlay> ); } import { DismissButton, Overlay, usePopover } from 'react-aria'; import type {AriaPopoverProps} from 'react-aria'; import type {OverlayTriggerState} from 'react-stately'; interface PopoverProps extends Omit<AriaPopoverProps, 'popoverRef'> { children: React.ReactNode; state: OverlayTriggerState; } function Popover( { children, state, ...props }: PopoverProps ) { let popoverRef = React.useRef(null); let { popoverProps, underlayProps } = usePopover({ ...props, popoverRef }, state); return ( <Overlay> <div {...underlayProps} style={{ position: 'fixed', inset: 0 }} /> <div {...popoverProps} ref={popoverRef} style={{ ...popoverProps.style, background: 'var(--page-background)', border: '1px solid gray' }} > <DismissButton onDismiss={state.close} /> {children} <DismissButton onDismiss={state.close} /> </div> </Overlay> ); } import { DismissButton, Overlay, usePopover } from 'react-aria'; import type {AriaPopoverProps} from 'react-aria'; import type {OverlayTriggerState} from 'react-stately'; interface PopoverProps extends Omit< AriaPopoverProps, 'popoverRef' > { children: React.ReactNode; state: OverlayTriggerState; } function Popover( { children, state, ...props }: PopoverProps ) { let popoverRef = React .useRef(null); let { popoverProps, underlayProps } = usePopover({ ...props, popoverRef }, state); return ( <Overlay> <div {...underlayProps} style={{ position: 'fixed', inset: 0 }} /> <div {...popoverProps} ref={popoverRef} style={{ ...popoverProps .style, background: 'var(--page-background)', border: '1px solid gray' }} > <DismissButton onDismiss={state .close} /> {children} <DismissButton onDismiss={state .close} /> </div> </Overlay> ); } ### Dialog# The `Dialog` component is rendered within the `Popover` component. It is built using the useDialog hook, and can be shared with many other components. Show code import type {AriaDialogProps} from 'react-aria'; import {useDialog} from 'react-aria'; interface DialogProps extends AriaDialogProps { title?: React.ReactNode; children: React.ReactNode; } function Dialog({ title, children, ...props }: DialogProps) { let ref = React.useRef(null); let { dialogProps, titleProps } = useDialog(props, ref); return ( <div {...dialogProps} ref={ref} style={{ padding: 30 }}> {title && ( <h3 {...titleProps} style={{ marginTop: 0 }}> {title} </h3> )} {children} </div> ); } import type {AriaDialogProps} from 'react-aria'; import {useDialog} from 'react-aria'; interface DialogProps extends AriaDialogProps { title?: React.ReactNode; children: React.ReactNode; } function Dialog( { title, children, ...props }: DialogProps ) { let ref = React.useRef(null); let { dialogProps, titleProps } = useDialog(props, ref); return ( <div {...dialogProps} ref={ref} style={{ padding: 30 }}> {title && ( <h3 {...titleProps} style={{ marginTop: 0 }}> {title} </h3> )} {children} </div> ); } import type {AriaDialogProps} from 'react-aria'; import {useDialog} from 'react-aria'; interface DialogProps extends AriaDialogProps { title?: React.ReactNode; children: React.ReactNode; } function Dialog( { title, children, ...props }: DialogProps ) { let ref = React.useRef( null ); let { dialogProps, titleProps } = useDialog( props, ref ); return ( <div {...dialogProps} ref={ref} style={{ padding: 30 }} > {title && ( <h3 {...titleProps} style={{ marginTop: 0 }} > {title} </h3> )} {children} </div> ); } ### RangeCalendar# The `RangeCalendar` component implements the month grid shown within the `DateRangePicker` popover. It can also be used standalone, or within other components. See useRangeCalendar for more examples and documentation. Show code import {useCalendarCell, useCalendarGrid, useRangeCalendar} from 'react-aria'; import {useRangeCalendarState} from 'react-stately'; import {createCalendar, getWeeksInMonth} from '@internationalized/date'; function RangeCalendar(props) { let { locale } = useLocale(); let state = useRangeCalendarState({ ...props, locale, createCalendar }); let ref = React.useRef(null); let { calendarProps, prevButtonProps, nextButtonProps, title } = useRangeCalendar(props, state, ref); return ( <div {...calendarProps} ref={ref} className="calendar"> <div className="header"> <h2>{title}</h2> <Button {...prevButtonProps}><</Button> <Button {...nextButtonProps}>></Button> </div> <CalendarGrid state={state} firstDayOfWeek={props.firstDayOfWeek} /> </div> ); } function CalendarGrid({ state, ...props }) { let { locale } = useLocale(); let { gridProps, headerProps, weekDays } = useCalendarGrid(props, state); // Get the number of weeks in the month so we can render the proper number of rows. let weeksInMonth = getWeeksInMonth( state.visibleRange.start, locale, props.firstDayOfWeek ); return ( <table {...gridProps}> <thead {...headerProps}> <tr> {weekDays.map((day, index) => <th key={index}>{day}</th>)} </tr> </thead> <tbody> {[...new Array(weeksInMonth).keys()].map((weekIndex) => ( <tr key={weekIndex}> {state.getDatesInWeek(weekIndex).map((date, i) => ( date ? ( <CalendarCell key={i} state={state} date={date} /> ) : <td key={i} /> ))} </tr> ))} </tbody> </table> ); } function CalendarCell({ state, date }) { let ref = React.useRef(null); let { cellProps, buttonProps, isSelected, isOutsideVisibleRange, isDisabled, isUnavailable, formattedDate } = useCalendarCell({ date }, state, ref); return ( <td {...cellProps}> <div {...buttonProps} ref={ref} hidden={isOutsideVisibleRange} className={`cell ${isSelected ? 'selected' : ''} ${ isDisabled ? 'disabled' : '' } ${isUnavailable ? 'unavailable' : ''}`} > {formattedDate} </div> </td> ); } import { useCalendarCell, useCalendarGrid, useRangeCalendar } from 'react-aria'; import {useRangeCalendarState} from 'react-stately'; import { createCalendar, getWeeksInMonth } from '@internationalized/date'; function RangeCalendar(props) { let { locale } = useLocale(); let state = useRangeCalendarState({ ...props, locale, createCalendar }); let ref = React.useRef(null); let { calendarProps, prevButtonProps, nextButtonProps, title } = useRangeCalendar(props, state, ref); return ( <div {...calendarProps} ref={ref} className="calendar"> <div className="header"> <h2>{title}</h2> <Button {...prevButtonProps}><</Button> <Button {...nextButtonProps}>></Button> </div> <CalendarGrid state={state} firstDayOfWeek={props.firstDayOfWeek} /> </div> ); } function CalendarGrid({ state, ...props }) { let { locale } = useLocale(); let { gridProps, headerProps, weekDays } = useCalendarGrid(props, state); // Get the number of weeks in the month so we can render the proper number of rows. let weeksInMonth = getWeeksInMonth( state.visibleRange.start, locale, props.firstDayOfWeek ); return ( <table {...gridProps}> <thead {...headerProps}> <tr> {weekDays.map((day, index) => ( <th key={index}>{day}</th> ))} </tr> </thead> <tbody> {[...new Array(weeksInMonth).keys()].map( (weekIndex) => ( <tr key={weekIndex}> {state.getDatesInWeek(weekIndex).map(( date, i ) => ( date ? ( <CalendarCell key={i} state={state} date={date} /> ) : <td key={i} /> ))} </tr> ) )} </tbody> </table> ); } function CalendarCell({ state, date }) { let ref = React.useRef(null); let { cellProps, buttonProps, isSelected, isOutsideVisibleRange, isDisabled, isUnavailable, formattedDate } = useCalendarCell({ date }, state, ref); return ( <td {...cellProps}> <div {...buttonProps} ref={ref} hidden={isOutsideVisibleRange} className={`cell ${isSelected ? 'selected' : ''} ${ isDisabled ? 'disabled' : '' } ${isUnavailable ? 'unavailable' : ''}`} > {formattedDate} </div> </td> ); } import { useCalendarCell, useCalendarGrid, useRangeCalendar } from 'react-aria'; import {useRangeCalendarState} from 'react-stately'; import { createCalendar, getWeeksInMonth } from '@internationalized/date'; function RangeCalendar( props ) { let { locale } = useLocale(); let state = useRangeCalendarState( { ...props, locale, createCalendar } ); let ref = React.useRef( null ); let { calendarProps, prevButtonProps, nextButtonProps, title } = useRangeCalendar( props, state, ref ); return ( <div {...calendarProps} ref={ref} className="calendar" > <div className="header"> <h2>{title}</h2> <Button {...prevButtonProps} > < </Button> <Button {...nextButtonProps} > > </Button> </div> <CalendarGrid state={state} firstDayOfWeek={props .firstDayOfWeek} /> </div> ); } function CalendarGrid( { state, ...props } ) { let { locale } = useLocale(); let { gridProps, headerProps, weekDays } = useCalendarGrid( props, state ); // Get the number of weeks in the month so we can render the proper number of rows. let weeksInMonth = getWeeksInMonth( state.visibleRange .start, locale, props .firstDayOfWeek ); return ( <table {...gridProps} > <thead {...headerProps} > <tr> {weekDays.map(( day, index ) => ( <th key={index} > {day} </th> ))} </tr> </thead> <tbody> {[...new Array( weeksInMonth ).keys()].map( (weekIndex) => ( <tr key={weekIndex} > {state .getDatesInWeek( weekIndex ).map(( date, i ) => ( date ? ( <CalendarCell key={i} state={state} date={date} /> ) : ( <td key={i} /> ) ))} </tr> ) )} </tbody> </table> ); } function CalendarCell( { state, date } ) { let ref = React.useRef( null ); let { cellProps, buttonProps, isSelected, isOutsideVisibleRange, isDisabled, isUnavailable, formattedDate } = useCalendarCell( { date }, state, ref ); return ( <td {...cellProps}> <div {...buttonProps} ref={ref} hidden={isOutsideVisibleRange} className={`cell ${ isSelected ? 'selected' : '' } ${ isDisabled ? 'disabled' : '' } ${ isUnavailable ? 'unavailable' : '' }`} > {formattedDate} </div> </td> ); } Show CSS .calendar { width: 220px; } .header { display: flex; align-items: center; gap: 4px; margin: 0 8px; } .header h2 { flex: 1; margin: 0; } .calendar table { width: 100%; } .cell { cursor: default; text-align: center; } .selected { background: var(--blue); color: white; } .unavailable { color: var(--spectrum-global-color-red-600); } .disabled { color: gray; } .calendar { width: 220px; } .header { display: flex; align-items: center; gap: 4px; margin: 0 8px; } .header h2 { flex: 1; margin: 0; } .calendar table { width: 100%; } .cell { cursor: default; text-align: center; } .selected { background: var(--blue); color: white; } .unavailable { color: var(--spectrum-global-color-red-600); } .disabled { color: gray; } .calendar { width: 220px; } .header { display: flex; align-items: center; gap: 4px; margin: 0 8px; } .header h2 { flex: 1; margin: 0; } .calendar table { width: 100%; } .cell { cursor: default; text-align: center; } .selected { background: var(--blue); color: white; } .unavailable { color: var(--spectrum-global-color-red-600); } .disabled { color: gray; } ## Styled examples# * * * Tailwind CSS A date range picker built with Tailwind and React Aria. Chakra UI A date and time range picker built with Chakra UI and React Aria. ## Usage# * * * The following examples show how to use the `DateRangePicker` component created in the above example. ### Value# A `DateRangePicker` displays a placeholder by default. An initial, uncontrolled value can be provided to the `DateRangePicker` using the `defaultValue` prop. Alternatively, a controlled value can be provided using the `value` prop. Date ranges are objects with `start` and `end` properties containing date values, which are provided using objects in the @internationalized/date package. This library handles correct international date manipulation across calendars, time zones, and other localization concerns. `useDateRangePicker` supports values of the following types: * ` CalendarDate `– a date without any time components. May be parsed from a string representation using the` parseDate `function. Use this type to represent dates where the time is not important, such as a birthday or an all day calendar event. * ` CalendarDateTime `– a date with a time, but not in any specific time zone. May be parsed from a string representation using the` parseDateTime `function. Use this type to represent times that occur at the same local time regardless of the time zone, such as the time of New Years Eve fireworks which always occur at midnight. Most times are better stored as a `ZonedDateTime`. * ` ZonedDateTime `– a date with a time in a specific time zone. May be parsed from a string representation using the` parseZonedDateTime `,` parseAbsolute `, or` parseAbsoluteToLocal `functions. Use this type to represent an exact moment in time at a particular location on Earth. import {parseDate} from '@internationalized/date'; function Example() { let [value, setValue] = React.useState({ start: parseDate('2020-02-03'), end: parseDate('2020-02-08') }); return ( <> <DateRangePicker label="Date range (uncontrolled)" defaultValue={{ start: parseDate('2020-02-03'), end: parseDate('2020-02-08') }} /> <DateRangePicker label="Date range (controlled)" value={value} onChange={setValue} /> </> ); } import {parseDate} from '@internationalized/date'; function Example() { let [value, setValue] = React.useState({ start: parseDate('2020-02-03'), end: parseDate('2020-02-08') }); return ( <> <DateRangePicker label="Date range (uncontrolled)" defaultValue={{ start: parseDate('2020-02-03'), end: parseDate('2020-02-08') }} /> <DateRangePicker label="Date range (controlled)" value={value} onChange={setValue} /> </> ); } import {parseDate} from '@internationalized/date'; function Example() { let [value, setValue] = React.useState({ start: parseDate( '2020-02-03' ), end: parseDate( '2020-02-08' ) }); return ( <> <DateRangePicker label="Date range (uncontrolled)" defaultValue={{ start: parseDate( '2020-02-03' ), end: parseDate( '2020-02-08' ) }} /> <DateRangePicker label="Date range (controlled)" value={value} onChange={setValue} /> </> ); } Date range (uncontrolled) 2 / 3 / 2020 – 2 / 8 / 2020 🗓 Date range (controlled) 2 / 3 / 2020 – 2 / 8 / 2020 🗓 ### Events# `useDateRangePicker` accepts an `onChange` prop which is triggered whenever the start or end date is edited by the user. The example below uses `onChange` to update a separate element with a formatted version of the date range in the user's locale and local time zone. This is done by converting the dates to native JavaScript `Date` objects to pass to the formatter. import {useDateFormatter} from 'react-aria'; import {getLocalTimeZone} from '@internationalized/date'; function Example() { let [range, setRange] = React.useState({ start: parseDate('2020-07-03'), end: parseDate('2020-07-10') }); let formatter = useDateFormatter({ dateStyle: 'long' }); return ( <> <DateRangePicker label="Date range" value={range} onChange={setRange} /> <p> Selected date: {range ? formatter.formatRange( range.start.toDate(getLocalTimeZone()), range.end.toDate(getLocalTimeZone()) ) : '--'} </p> </> ); } import {useDateFormatter} from 'react-aria'; import {getLocalTimeZone} from '@internationalized/date'; function Example() { let [range, setRange] = React.useState({ start: parseDate('2020-07-03'), end: parseDate('2020-07-10') }); let formatter = useDateFormatter({ dateStyle: 'long' }); return ( <> <DateRangePicker label="Date range" value={range} onChange={setRange} /> <p> Selected date: {range ? formatter.formatRange( range.start.toDate(getLocalTimeZone()), range.end.toDate(getLocalTimeZone()) ) : '--'} </p> </> ); } import {useDateFormatter} from 'react-aria'; import {getLocalTimeZone} from '@internationalized/date'; function Example() { let [range, setRange] = React.useState({ start: parseDate( '2020-07-03' ), end: parseDate( '2020-07-10' ) }); let formatter = useDateFormatter({ dateStyle: 'long' }); return ( <> <DateRangePicker label="Date range" value={range} onChange={setRange} /> <p> Selected date: {' '} {range ? formatter .formatRange( range.start .toDate( getLocalTimeZone() ), range.end .toDate( getLocalTimeZone() ) ) : '--'} </p> </> ); } Date range 7 / 3 / 2020 – 7 / 10 / 2020 🗓 Selected date: July 3 – 10, 2020 ### Time zones# `useDateRangePicker` is time zone aware when `ZonedDateTime` objects are provided as the value. In this case, the time zone abbreviation is displayed, and time zone concerns such as daylight saving time are taken into account when the value is manipulated. In most cases, your data will come from and be sent to a server as an ISO 8601 formatted string. @internationalized/date includes functions for parsing strings in multiple formats into `ZonedDateTime` objects. Which format you use will depend on what information you need to store. * ` parseZonedDateTime `– This function parses a date with an explicit time zone and optional UTC offset attached (e.g. `"2021-11-07T00:45[America/Los_Angeles]"` or `"2021-11-07T00:45-07:00[America/Los_Angeles]"`). This format preserves the maximum amount of information. If the exact local time and time zone that a user selected is important, use this format. Storing the time zone and offset that was selected rather than converting to UTC ensures that the local time is correct regardless of daylight saving rule changes (e.g. if a locale abolishes DST). Examples where this applies include calendar events, reminders, and other times that occur in a particular location. * ` parseAbsolute `– This function parses an absolute date and time that occurs at the same instant at all locations on Earth. It can be represented in UTC (e.g. `"2021-11-07T07:45:00Z"`), or stored with a particular offset (e.g. `"2021-11-07T07:45:00-07:00"`). A time zone identifier, e.g. `America/Los_Angeles`, must be passed, and the result will be converted into that time zone. Absolute times are the best way to represent events that occurred in the past, or future events where an exact time is needed, regardless of time zone. * ` parseAbsoluteToLocal `– This function parses an absolute date and time into the current user's local time zone. It is a shortcut for `parseAbsolute`, and accepts the same formats. import {parseZonedDateTime} from '@internationalized/date'; <DateRangePicker label="Date range" defaultValue={{ start: parseZonedDateTime('2022-11-07T00:45[America/Los_Angeles]'), end: parseZonedDateTime('2022-11-08T11:15[America/Los_Angeles]') }} /> import {parseZonedDateTime} from '@internationalized/date'; <DateRangePicker label="Date range" defaultValue={{ start: parseZonedDateTime( '2022-11-07T00:45[America/Los_Angeles]' ), end: parseZonedDateTime( '2022-11-08T11:15[America/Los_Angeles]' ) }} /> import {parseZonedDateTime} from '@internationalized/date'; <DateRangePicker label="Date range" defaultValue={{ start: parseZonedDateTime( '2022-11-07T00:45[America/Los_Angeles]' ), end: parseZonedDateTime( '2022-11-08T11:15[America/Los_Angeles]' ) }} /> Date range 11 / 7 / 2022 , 12 : 45 AM PST – 11 / 8 / 2022 , 11 : 15 AM PST 🗓 `useDateRangePicker` displays times in the time zone included in the `ZonedDateTime` object. The above example is always displayed in Pacific Standard Time because the `America/Los_Angeles` time zone identifier is provided. @internationalized/date includes functions for converting dates between time zones, or parsing a date directly into a specific time zone or the user's local time zone, as shown below. import {parseAbsoluteToLocal} from '@internationalized/date'; <DateRangePicker label="Date range" defaultValue={{ start: parseAbsoluteToLocal('2021-11-07T07:45:00Z'), end: parseAbsoluteToLocal('2021-11-08T14:25:00Z') }} /> import {parseAbsoluteToLocal} from '@internationalized/date'; <DateRangePicker label="Date range" defaultValue={{ start: parseAbsoluteToLocal('2021-11-07T07:45:00Z'), end: parseAbsoluteToLocal('2021-11-08T14:25:00Z') }} /> import {parseAbsoluteToLocal} from '@internationalized/date'; <DateRangePicker label="Date range" defaultValue={{ start: parseAbsoluteToLocal( '2021-11-07T07:45:00Z' ), end: parseAbsoluteToLocal( '2021-11-08T14:25:00Z' ) }} /> Date range 11 / 7 / 2021 , 7 : 45 AM UTC – 11 / 8 / 2021 , 2 : 25 PM UTC 🗓 ### Granularity# The `granularity` prop allows you to control the smallest unit that is displayed by `useDateRangePicker`. By default, `CalendarDate` values are displayed with `"day"` granularity (year, month, and day), and `CalendarDateTime` and `ZonedDateTime` values are displayed with `"minute"` granularity. More granular time values can be displayed by setting the `granularity` prop to `"second"`. In addition, when a value with a time is provided but you wish to only display the date, you can set the granularity to `"day"`. This has no effect on the actual value (it still has a time component), only on what fields are displayed. In the following example, two DateRangePickers are synchronized with the same value, but display different granularities. function Example() { let [date, setDate] = React.useState({ start: parseAbsoluteToLocal('2021-04-07T18:45:22Z'), end: parseAbsoluteToLocal('2021-04-08T20:00:00Z') }); return ( <> <DateRangePicker label="Date and time range" granularity="second" value={date} onChange={setDate} /> <DateRangePicker label="Date range" granularity="day" value={date} onChange={setDate} /> </> ); } function Example() { let [date, setDate] = React.useState({ start: parseAbsoluteToLocal('2021-04-07T18:45:22Z'), end: parseAbsoluteToLocal('2021-04-08T20:00:00Z') }); return ( <> <DateRangePicker label="Date and time range" granularity="second" value={date} onChange={setDate} /> <DateRangePicker label="Date range" granularity="day" value={date} onChange={setDate} /> </> ); } function Example() { let [date, setDate] = React.useState({ start: parseAbsoluteToLocal( '2021-04-07T18:45:22Z' ), end: parseAbsoluteToLocal( '2021-04-08T20:00:00Z' ) }); return ( <> <DateRangePicker label="Date and time range" granularity="second" value={date} onChange={setDate} /> <DateRangePicker label="Date range" granularity="day" value={date} onChange={setDate} /> </> ); } Date and time range 4 / 7 / 2021 , 6 : 45 : 22 PM UTC – 4 / 8 / 2021 , 8 : 00 : 00 PM UTC 🗓 Date range 4 / 7 / 2021 – 4 / 8 / 2021 🗓 If no `value` or `defaultValue` prop is passed, then the `granularity` prop also affects which type of value is emitted from the `onChange` event. Note that by default, time values will not have a time zone because none was supplied. You can override this by setting the `placeholderValue` prop explicitly. Values emitted from `onChange` will use the time zone of the placeholder value. import {now} from '@internationalized/date'; <DateRangePicker label="Date range" granularity="second" /> <DateRangePicker label="Date range" placeholderValue={now('America/New_York')} granularity="second" /> import {now} from '@internationalized/date'; <DateRangePicker label="Date range" granularity="second" /> <DateRangePicker label="Date range" placeholderValue={now('America/New_York')} granularity="second" /> import {now} from '@internationalized/date'; <DateRangePicker label="Date range" granularity="second" /> <DateRangePicker label="Date range" placeholderValue={now( 'America/New_York' )} granularity="second" /> Date range mm / dd / yyyy , –– : –– : –– AM – mm / dd / yyyy , –– : –– : –– AM 🗓 Date range mm / dd / yyyy , –– : –– : –– AM EDT – mm / dd / yyyy , –– : –– : –– AM EDT 🗓 ### International calendars# `useDateRangePicker` supports selecting dates in many calendar systems used around the world, including Gregorian, Hebrew, Indian, Islamic, Buddhist, and more. Dates are automatically displayed in the appropriate calendar system for the user's locale. The calendar system can be overridden using the Unicode calendar locale extension, passed to the I18nProvider component. Selected dates passed to `onChange` always use the same calendar system as the `value` or `defaultValue` prop. If no `value` or `defaultValue` is provided, then dates passed to `onChange` are always in the Gregorian calendar since this is the most commonly used. This means that even though the user selects dates in their local calendar system, applications are able to deal with dates from all users consistently. The below example displays a `DateRangePicker` in the Hindi language, using the Indian calendar. Dates emitted from `onChange` are in the Gregorian calendar. import {I18nProvider} from 'react-aria'; function Example() { let [range, setRange] = React.useState(null); return ( <I18nProvider locale="hi-IN-u-ca-indian"> <DateRangePicker label="Date range" value={range} onChange={setRange} /> <p>Start date: {range?.start.toString()}</p> <p>End date: {range?.end.toString()}</p> </I18nProvider> ); } import {I18nProvider} from 'react-aria'; function Example() { let [range, setRange] = React.useState(null); return ( <I18nProvider locale="hi-IN-u-ca-indian"> <DateRangePicker label="Date range" value={range} onChange={setRange} /> <p>Start date: {range?.start.toString()}</p> <p>End date: {range?.end.toString()}</p> </I18nProvider> ); } import {I18nProvider} from 'react-aria'; function Example() { let [range, setRange] = React.useState(null); return ( <I18nProvider locale="hi-IN-u-ca-indian"> <DateRangePicker label="Date range" value={range} onChange={setRange} /> <p> Start date:{' '} {range?.start .toString()} </p> <p> End date:{' '} {range?.end .toString()} </p> </I18nProvider> ); } Date range dd / mm / yyyy शक – dd / mm / yyyy शक 🗓 Start date: End date: ### Minimum and maximum values# The `minValue` and `maxValue` props can also be used to perform builtin validation. This prevents the user from selecting dates outside the valid range in the calendar, and marks the date fields as invalid using ARIA. `useDateRangePicker` also validates that the end date is after the start date. You should implement a visual indication that the date range picker is invalid as well. This example only accepts dates after today. import {today} from '@internationalized/date'; <DateRangePicker label="Trip dates" minValue={today(getLocalTimeZone())} defaultValue={{ start: parseDate('2022-02-03'), end: parseDate('2022-05-03') }} /> import {today} from '@internationalized/date'; <DateRangePicker label="Trip dates" minValue={today(getLocalTimeZone())} defaultValue={{ start: parseDate('2022-02-03'), end: parseDate('2022-05-03') }} /> import {today} from '@internationalized/date'; <DateRangePicker label="Trip dates" minValue={today( getLocalTimeZone() )} defaultValue={{ start: parseDate( '2022-02-03' ), end: parseDate( '2022-05-03' ) }} /> Trip dates 2 / 3 / 2022 – 5 / 3 / 2022 🚫 🗓 ### Unavailable dates# `useDateRangePicker` supports marking certain dates as _unavailable_. These dates remain focusable with the keyboard in the calendar so that navigation is consistent, but cannot be selected by the user. The `isDateUnavailable` prop accepts a callback that is called to evaluate whether each visible date is unavailable. Note that by default, users may not select non-contiguous ranges, i.e. ranges that contain unavailable dates within them. Once a start date is selected in the calendar, enabled dates will be restricted to subsequent dates until an unavailable date is hit. While this is handled automatically in the calendar, additional validation logic must be provided to ensure an invalid state is displayed in the date field. This can be achieved using the `isInvalid` prop. See below for an example of how to allow non-contiguous ranges. This example includes multiple unavailable date ranges, e.g. dates when a rental house is not available. The `minValue` prop is also used to prevent selecting dates before today. The `isInvalid` prop is used to mark selected date ranges with unavailable dates in the middle as invalid. import {today} from '@internationalized/date'; function Example() { let now = today(getLocalTimeZone()); let disabledRanges = [ [now, now.add({ days: 5 })], [now.add({ days: 14 }), now.add({ days: 16 })], [now.add({ days: 23 }), now.add({ days: 24 })] ]; let isDateUnavailable = (date) => disabledRanges.some((interval) => date.compare(interval[0]) >= 0 && date.compare(interval[1]) <= 0 ); let [value, setValue] = React.useState(null); let isInvalid = value && disabledRanges.some((interval) => value.end.compare(interval[0]) >= 0 && value.start.compare(interval[1]) <= 0 ); return ( <DateRangePicker label="Trip dates" minValue={today(getLocalTimeZone())} isDateUnavailable={isDateUnavailable} value={value} onChange={setValue} isInvalid={isInvalid} /> ); } import {today} from '@internationalized/date'; function Example() { let now = today(getLocalTimeZone()); let disabledRanges = [ [now, now.add({ days: 5 })], [now.add({ days: 14 }), now.add({ days: 16 })], [now.add({ days: 23 }), now.add({ days: 24 })] ]; let isDateUnavailable = (date) => disabledRanges.some((interval) => date.compare(interval[0]) >= 0 && date.compare(interval[1]) <= 0 ); let [value, setValue] = React.useState(null); let isInvalid = value && disabledRanges.some((interval) => value.end.compare(interval[0]) >= 0 && value.start.compare(interval[1]) <= 0 ); return ( <DateRangePicker label="Trip dates" minValue={today(getLocalTimeZone())} isDateUnavailable={isDateUnavailable} value={value} onChange={setValue} isInvalid={isInvalid} /> ); } import {today} from '@internationalized/date'; function Example() { let now = today( getLocalTimeZone() ); let disabledRanges = [ [ now, now.add({ days: 5 }) ], [ now.add({ days: 14 }), now.add({ days: 16 }) ], [ now.add({ days: 23 }), now.add({ days: 24 }) ] ]; let isDateUnavailable = (date) => disabledRanges .some(( interval ) => date.compare( interval[0] ) >= 0 && date.compare( interval[1] ) <= 0 ); let [value, setValue] = React.useState(null); let isInvalid = value && disabledRanges.some( (interval) => value.end .compare( interval[0] ) >= 0 && value.start .compare( interval[1] ) <= 0 ); return ( <DateRangePicker label="Trip dates" minValue={today( getLocalTimeZone() )} isDateUnavailable={isDateUnavailable} value={value} onChange={setValue} isInvalid={isInvalid} /> ); } Trip dates mm / dd / yyyy – mm / dd / yyyy 🗓 ### Non-contiguous ranges# The `allowsNonContiguousRanges` prop enables a range to be selected even if there are unavailable dates in the middle. The value emitted in the `onChange` event will still be a single range with a `start` and `end` property, but unavailable dates will not be displayed as selected. It is up to applications to split the full selected range into multiple as needed for business logic. This example prevents selecting weekends, but allows selecting ranges that span multiple weeks. import {isWeekend} from '@internationalized/date'; function Example() { let { locale } = useLocale(); return ( <DateRangePicker label="Time off request" isDateUnavailable={(date) => isWeekend(date, locale)} allowsNonContiguousRanges /> ); } import {isWeekend} from '@internationalized/date'; function Example() { let { locale } = useLocale(); return ( <DateRangePicker label="Time off request" isDateUnavailable={(date) => isWeekend(date, locale)} allowsNonContiguousRanges /> ); } import {isWeekend} from '@internationalized/date'; function Example() { let { locale } = useLocale(); return ( <DateRangePicker label="Time off request" isDateUnavailable={(date) => isWeekend( date, locale )} allowsNonContiguousRanges /> ); } Time off request mm / dd / yyyy – mm / dd / yyyy 🗓 ### Placeholder value# When no value is set, a placeholder is shown. The format of the placeholder is influenced by the `granularity` and `placeholderValue` props. `placeholderValue` also controls the default values of each segment when the user first interacts with them, e.g. using the up and down arrow keys, as well as the default month shown in the calendar popover. By default, the `placeholderValue` is the current date at midnight, but you can set it to a more appropriate value if needed. import {CalendarDate} from '@internationalized/date'; <DateRangePicker label="Date range" placeholderValue={new CalendarDate(1980, 1, 1)} /> import {CalendarDate} from '@internationalized/date'; <DateRangePicker label="Date range" placeholderValue={new CalendarDate(1980, 1, 1)} /> import {CalendarDate} from '@internationalized/date'; <DateRangePicker label="Date range" placeholderValue={new CalendarDate( 1980, 1, 1 )} /> Date range mm / dd / yyyy – mm / dd / yyyy 🗓 ### Hide time zone# When `ZonedDateTime` objects are provided as the value of to `useDateRangePicker`, the time zone abbreviation is displayed by default. However, if this is displayed elsewhere or implicit based on the usecase, it can be hidden using the `hideTimeZone` option. <DateRangePicker label="Date range" defaultValue={{ start: parseZonedDateTime('2022-11-07T10:45[America/Los_Angeles]'), end: parseZonedDateTime('2022-11-08T19:45[America/Los_Angeles]') }} hideTimeZone /> <DateRangePicker label="Date range" defaultValue={{ start: parseZonedDateTime( '2022-11-07T10:45[America/Los_Angeles]' ), end: parseZonedDateTime( '2022-11-08T19:45[America/Los_Angeles]' ) }} hideTimeZone /> <DateRangePicker label="Date range" defaultValue={{ start: parseZonedDateTime( '2022-11-07T10:45[America/Los_Angeles]' ), end: parseZonedDateTime( '2022-11-08T19:45[America/Los_Angeles]' ) }} hideTimeZone /> Date range 11 / 7 / 2022 , 10 : 45 AM – 11 / 8 / 2022 , 7 : 45 PM 🗓 ### Hour cycle# By default, `useDateRangePicker` displays times in either 12 or 24 hour hour format depending on the user's locale. However, this can be overridden using the `hourCycle` prop if needed for a specific usecase. This example forces the `DateRangePicker` to use 24-hour time, regardless of the locale. <DateRangePicker label="Date range" granularity="minute" hourCycle={24} /> <DateRangePicker label="Date range" granularity="minute" hourCycle={24} /> <DateRangePicker label="Date range" granularity="minute" hourCycle={24} /> Date range mm / dd / yyyy , –– : –– – mm / dd / yyyy , –– : –– 🗓 ### Custom first day of week# By default, the first day of the week is automatically set based on the current locale. This can be changed by setting the `firstDayOfWeek` prop to `'sun'`, `'mon'`, `'tue'`, `'wed'`, `'thu'`, `'fri'`, or `'sat'`. <DateRangePicker label="Date range" firstDayOfWeek="mon" /> <DateRangePicker label="Date range" firstDayOfWeek="mon" /> <DateRangePicker label="Date range" firstDayOfWeek="mon" /> Date range mm / dd / yyyy – mm / dd / yyyy 🗓 --- ## Page: https://react-spectrum.adobe.com/react-aria/useRangeCalendar.html # useRangeCalendar Provides the behavior and accessibility implementation for a range calendar component. A range calendar displays one or more date grids and allows users to select a contiguous range of dates. <table><tbody><tr><th>install</th><td><code>yarn add react-aria</code></td></tr><tr><th>version</th><td>3.41.1</td></tr><tr><th>usage</th><td><code><span>import</span> {useRangeCalendar} <span>from</span> <span>'react-aria'</span></code></td></tr></tbody></table> View repository GitHub View package NPM ## API# * * * `useRangeCalendar<T extends DateValue >( props: AriaRangeCalendarProps <T>, state: RangeCalendarState , ref: RefObject <FocusableElement | | null> ): CalendarAria ``useCalendarGrid( (props: AriaCalendarGridProps , , state: CalendarState | | RangeCalendarState )): CalendarGridAria ``useCalendarCell( props: AriaCalendarCellProps , state: CalendarState | | RangeCalendarState , ref: RefObject <HTMLElement | | null> ): CalendarCellAria ` ## Features# * * * There is no standalone range calendar element in HTML. Two separate `<input type="date">` elements could be used, but this is very limited in functionality, lacking in internationalization capabilities, inconsistent between browsers, and difficult to style. `useRangeCalendar` helps achieve accessible and international range calendar components that can be styled as needed. * **Flexible** – Display one or more months at once, or a custom time range for use cases like a week view. Minimum and maximum values, unavailable dates, and non-contiguous selections are supported as well. * **International** – Support for 13 calendar systems used around the world, including Gregorian, Buddhist, Islamic, Persian, and more. Locale-specific formatting, number systems, and right-to-left support are available as well. * **Accessible** – Calendar cells can be navigated and selected using the keyboard, and localized screen reader messages are included to announce when the selection and visible date range change. * **Touch friendly** – Date ranges can be selected by dragging over dates in the calendar using a touch screen, and all interactions are accessible using touch-based screen readers. * **Customizable** – As with all of React Aria, the DOM structure and styling of all elements can be fully customized. Read our blog post for more details about the internationalization, accessibility, and user experience features implemented by `useRangeCalendar`. ## Anatomy# * * * A range calendar consists of a grouping element containing one or more date grids (e.g. months), and a previous and next button for navigating through time. Each calendar grid consists of cells containing button elements that can be pressed and navigated to using the arrow keys to select a date range. Once a start date is selected, the user can navigate to another date using the keyboard or by hovering over it, and clicking it or pressing the Enter key commits the selected date range. ### useRangeCalendar# `useRangeCalendar` returns props that you should spread onto the appropriate elements: | Name | Type | Description | | --- | --- | --- | | `calendarProps` | `DOMAttributes` | Props for the calendar grouping element. | | `nextButtonProps` | ` AriaButtonProps ` | Props for the next button. | | `prevButtonProps` | ` AriaButtonProps ` | Props for the previous button. | | `errorMessageProps` | `DOMAttributes` | Props for the error message element, if any. | | `title` | `string` | A description of the visible date range, for use in the calendar title. | ### useCalendarGrid# `useCalendarGrid` returns props for an individual grid of dates, such as one month, along with a list of formatted weekday names in the current locale for use during rendering: | Name | Type | Description | | --- | --- | --- | | `gridProps` | `DOMAttributes` | Props for the date grid element (e.g. `<table>`). | | `headerProps` | `DOMAttributes` | Props for the grid header element (e.g. `<thead>`). | | `weekDays` | `string[]` | A list of week day abbreviations formatted for the current locale, typically used in column headers. | | `weeksInMonth` | `number` | The number of weeks in the month. | ### useCalendarCell# `useCalendarCell` returns props for an individual cell, along with states and information useful during rendering: | Name | Type | Description | | --- | --- | --- | | `cellProps` | `DOMAttributes` | Props for the grid cell element (e.g. `<td>`). | | `buttonProps` | `DOMAttributes` | Props for the button element within the cell. | | `isPressed` | `boolean` | Whether the cell is currently being pressed. | | `isSelected` | `boolean` | Whether the cell is selected. | | `isFocused` | `boolean` | Whether the cell is focused. | | `isDisabled` | `boolean` | Whether the cell is disabled, according to the calendar's `minValue`, `maxValue`, and `isDisabled` props. Disabled dates are not focusable, and cannot be selected by the user. They are typically displayed with a dimmed appearance. | | `isUnavailable` | `boolean` | Whether the cell is unavailable, according to the calendar's `isDateUnavailable` prop. Unavailable dates remain focusable, but cannot be selected by the user. They should be displayed with a visual affordance to indicate they are unavailable, such as a different color or a strikethrough. Note that because they are focusable, unavailable dates must meet a 4.5:1 color contrast ratio, as defined by WCAG. | | `isOutsideVisibleRange` | `boolean` | Whether the cell is outside the visible range of the calendar. For example, dates before the first day of a month in the same week. | | `isInvalid` | `boolean` | Whether the cell is part of an invalid selection. | | `formattedDate` | `string` | The day number formatted according to the current locale. | State is managed by the `useRangeCalendarState` hook from `@react-stately/calendar`. The state object should be passed as an option to `useRangeCalendar`, `useCalendarGrid`, and `useCalendarCell`. Note that much of this anatomy is shared with non-range calendars. The only difference is that `useRangeCalendarState` is used instead of `useCalendarState`, and `useRangeCalendar` is used instead of `useCalendar`. ## Date and time values# * * * Dates are represented in many different ways by cultures around the world. This includes differences in calendar systems, date formatting, numbering systems, weekday and weekend rules, and much more. When building applications that support users around the world, it is important to handle these aspects correctly for each locale. `useRangeCalendar` uses the @internationalized/date library to represent dates and times. This package provides a library of objects and functions to perform date and time related manipulation, queries, and conversions that work across locales and calendars. Date and time objects can be converted to and from native JavaScript `Date` objects or ISO 8601 strings. See the documentation, or the examples below for more details. `useRangeCalendarState` requires a `createCalendar` function to be provided, which is used to implement date manipulation across multiple calendar systems. The default implementation in `@internationalized/date` includes all supported calendar systems. While this library is quite small (8 kB minified + Brotli), you can reduce its bundle size further by providing your own implementation that includes only your supported calendars. See below for an example. ## Example# * * * A `RangeCalendar` consists of three components: the main calendar wrapper element with previous and next buttons for navigating, one or more `CalendarGrid` components to display each month, and `CalendarCell` components for each date cell. We'll go through them one by one. For simplicity, this example only displays a single month at a time. See the styled examples section for more examples with multiple months, as well as other time ranges like weeks. import {useLocale, useRangeCalendar} from 'react-aria'; import {useRangeCalendarState} from 'react-stately'; import {createCalendar} from '@internationalized/date'; // Reuse the Button from your component library. See below for details. import {Button} from 'your-component-library'; function RangeCalendar(props) { let { locale } = useLocale(); let state = useRangeCalendarState({ createCalendar, ...props, locale }); let ref = React.useRef(null); let { calendarProps, prevButtonProps, nextButtonProps, title } = useRangeCalendar(props, state, ref); return ( <div {...calendarProps} ref={ref} className="calendar"> <div className="header"> <h2>{title}</h2> <Button {...prevButtonProps}><</Button> <Button {...nextButtonProps}>></Button> </div> <CalendarGrid state={state} firstDayOfWeek={props.firstDayOfWeek} /> </div> ); } import {useLocale, useRangeCalendar} from 'react-aria'; import {useRangeCalendarState} from 'react-stately'; import {createCalendar} from '@internationalized/date'; // Reuse the Button from your component library. See below for details. import {Button} from 'your-component-library'; function RangeCalendar(props) { let { locale } = useLocale(); let state = useRangeCalendarState({ createCalendar, ...props, locale }); let ref = React.useRef(null); let { calendarProps, prevButtonProps, nextButtonProps, title } = useRangeCalendar(props, state, ref); return ( <div {...calendarProps} ref={ref} className="calendar"> <div className="header"> <h2>{title}</h2> <Button {...prevButtonProps}><</Button> <Button {...nextButtonProps}>></Button> </div> <CalendarGrid state={state} firstDayOfWeek={props.firstDayOfWeek} /> </div> ); } import { useLocale, useRangeCalendar } from 'react-aria'; import {useRangeCalendarState} from 'react-stately'; import {createCalendar} from '@internationalized/date'; // Reuse the Button from your component library. See below for details. import {Button} from 'your-component-library'; function RangeCalendar( props ) { let { locale } = useLocale(); let state = useRangeCalendarState( { createCalendar, ...props, locale } ); let ref = React.useRef( null ); let { calendarProps, prevButtonProps, nextButtonProps, title } = useRangeCalendar( props, state, ref ); return ( <div {...calendarProps} ref={ref} className="calendar" > <div className="header"> <h2>{title}</h2> <Button {...prevButtonProps} > < </Button> <Button {...nextButtonProps} > > </Button> </div> <CalendarGrid state={state} firstDayOfWeek={props .firstDayOfWeek} /> </div> ); } ### CalendarGrid# The `CalendarGrid` component will be responsible for rendering an individual month. It is a separate component so that you can render more than one month at a time if you like. It's rendered as an HTML `<table>` element, and React Aria takes care of adding the proper ARIA roles and event handlers to make it behave as an ARIA grid. You can use the arrow keys to navigate between cells, and the Enter key to select a date. The `state.getDatesInWeek` function returns the dates in each week of the month. Note that this always includes 7 values, but some of them may be null, which indicates that the date doesn't exist within the calendar system. You should render a placeholder `<td>` element in this case so that the cells line up correctly. **Note**: this component is the same as the `CalendarGrid` component shown in the useCalendar docs, and you can reuse it between both `Calendar` and `RangeCalendar`. import {useCalendarGrid} from 'react-aria'; function CalendarGrid({ state, ...props }) { let { gridProps, headerProps, weekDays, weeksInMonth } = useCalendarGrid( props, state ); return ( <table {...gridProps}> <thead {...headerProps}> <tr> {weekDays.map((day, index) => <th key={index}>{day}</th>)} </tr> </thead> <tbody> {[...new Array(weeksInMonth).keys()].map((weekIndex) => ( <tr key={weekIndex}> {state.getDatesInWeek(weekIndex).map((date, i) => ( date ? ( <CalendarCell key={i} state={state} date={date} /> ) : <td key={i} /> ))} </tr> ))} </tbody> </table> ); } import {useCalendarGrid} from 'react-aria'; function CalendarGrid({ state, ...props }) { let { gridProps, headerProps, weekDays, weeksInMonth } = useCalendarGrid(props, state); return ( <table {...gridProps}> <thead {...headerProps}> <tr> {weekDays.map((day, index) => ( <th key={index}>{day}</th> ))} </tr> </thead> <tbody> {[...new Array(weeksInMonth).keys()].map( (weekIndex) => ( <tr key={weekIndex}> {state.getDatesInWeek(weekIndex).map(( date, i ) => ( date ? ( <CalendarCell key={i} state={state} date={date} /> ) : <td key={i} /> ))} </tr> ) )} </tbody> </table> ); } import {useCalendarGrid} from 'react-aria'; function CalendarGrid( { state, ...props } ) { let { gridProps, headerProps, weekDays, weeksInMonth } = useCalendarGrid( props, state ); return ( <table {...gridProps} > <thead {...headerProps} > <tr> {weekDays.map(( day, index ) => ( <th key={index} > {day} </th> ))} </tr> </thead> <tbody> {[...new Array( weeksInMonth ).keys()].map( (weekIndex) => ( <tr key={weekIndex} > {state .getDatesInWeek( weekIndex ).map(( date, i ) => ( date ? ( <CalendarCell key={i} state={state} date={date} /> ) : ( <td key={i} /> ) ))} </tr> ) )} </tbody> </table> ); } ### CalendarCell# Finally, the `CalendarCell` component renders an individual cell in a calendar. It consists of two elements: a `<td>` to represent the grid cell, and a `<div>` to represent a button that can be clicked to select the date. The `useCalendarCell` hook also returns the formatted date string in the current locale, as well as some information about the cell's state that can be useful for styling. See above for details. **Note**: this component is the same as the `CalendarCell` component shown in the useCalendar docs, and you can reuse it between both `Calendar` and `RangeCalendar`. import {useCalendarCell} from 'react-aria'; function CalendarCell({ state, date }) { let ref = React.useRef(null); let { cellProps, buttonProps, isSelected, isOutsideVisibleRange, isDisabled, isUnavailable, formattedDate } = useCalendarCell({ date }, state, ref); return ( <td {...cellProps}> <div {...buttonProps} ref={ref} hidden={isOutsideVisibleRange} className={`cell ${isSelected ? 'selected' : ''} ${ isDisabled ? 'disabled' : '' } ${isUnavailable ? 'unavailable' : ''}`} > {formattedDate} </div> </td> ); } import {useCalendarCell} from 'react-aria'; function CalendarCell({ state, date }) { let ref = React.useRef(null); let { cellProps, buttonProps, isSelected, isOutsideVisibleRange, isDisabled, isUnavailable, formattedDate } = useCalendarCell({ date }, state, ref); return ( <td {...cellProps}> <div {...buttonProps} ref={ref} hidden={isOutsideVisibleRange} className={`cell ${isSelected ? 'selected' : ''} ${ isDisabled ? 'disabled' : '' } ${isUnavailable ? 'unavailable' : ''}`} > {formattedDate} </div> </td> ); } import {useCalendarCell} from 'react-aria'; function CalendarCell( { state, date } ) { let ref = React.useRef( null ); let { cellProps, buttonProps, isSelected, isOutsideVisibleRange, isDisabled, isUnavailable, formattedDate } = useCalendarCell( { date }, state, ref ); return ( <td {...cellProps}> <div {...buttonProps} ref={ref} hidden={isOutsideVisibleRange} className={`cell ${ isSelected ? 'selected' : '' } ${ isDisabled ? 'disabled' : '' } ${ isUnavailable ? 'unavailable' : '' }`} > {formattedDate} </div> </td> ); } That's it! Now we can render an example of our `RangeCalendar` component in action. <RangeCalendar aria-label="Trip dates" /> <RangeCalendar aria-label="Trip dates" /> <RangeCalendar aria-label="Trip dates" /> ## June 2025 <\> | S | M | T | W | T | F | S | | --- | --- | --- | --- | --- | --- | --- | | 1 | 2 | 3 | 4 | 5 | 6 | 7 | | 8 | 9 | 10 | 11 | 12 | 13 | 14 | | 15 | 16 | 17 | 18 | 19 | 20 | 21 | | 22 | 23 | 24 | 25 | 26 | 27 | 28 | | 29 | 30 | 1 | 2 | 3 | 4 | 5 | Show CSS .calendar { width: 220px; } .header { display: flex; align-items: center; gap: 4px; margin: 0 8px; } .header h2 { flex: 1; margin: 0; } .calendar table { width: 100%; } .cell { cursor: default; text-align: center; } .selected { background: var(--blue); color: white; } .unavailable { color: var(--spectrum-global-color-red-600); } .disabled { color: gray; } .calendar { width: 220px; } .header { display: flex; align-items: center; gap: 4px; margin: 0 8px; } .header h2 { flex: 1; margin: 0; } .calendar table { width: 100%; } .cell { cursor: default; text-align: center; } .selected { background: var(--blue); color: white; } .unavailable { color: var(--spectrum-global-color-red-600); } .disabled { color: gray; } .calendar { width: 220px; } .header { display: flex; align-items: center; gap: 4px; margin: 0 8px; } .header h2 { flex: 1; margin: 0; } .calendar table { width: 100%; } .cell { cursor: default; text-align: center; } .selected { background: var(--blue); color: white; } .unavailable { color: var(--spectrum-global-color-red-600); } .disabled { color: gray; } ### Button# The `Button` component is used in the above example to navigate between months. It is built using the useButton hook, and can be shared with many other components. Show code import {useButton} from 'react-aria'; function Button(props) { let ref = React.useRef(null); let { buttonProps } = useButton(props, ref); return <button {...buttonProps} ref={ref}>{props.children}</button>; } import {useButton} from 'react-aria'; function Button(props) { let ref = React.useRef(null); let { buttonProps } = useButton(props, ref); return ( <button {...buttonProps} ref={ref}> {props.children} </button> ); } import {useButton} from 'react-aria'; function Button(props) { let ref = React.useRef( null ); let { buttonProps } = useButton( props, ref ); return ( <button {...buttonProps} ref={ref} > {props.children} </button> ); } ## Styled examples# * * * Tailwind CSS A RangeCalendar built with Tailwind, supporting multiple visible months. ## Usage# * * * The following examples show how to use the `RangeCalendar` component created in the above example. ### Value# A `RangeCalendar` has no selection by default. An initial, uncontrolled value can be provided to the `RangeCalendar` using the `defaultValue` prop. Alternatively, a controlled value can be provided using the `value` prop. Date ranges are objects with `start` and `end` properties containing date values, which are provided using objects in the @internationalized/date package. This library handles correct international date manipulation across calendars, time zones, and other localization concerns. `useRangeCalendar` supports values with both date and time components, but only allows users to modify the dates. By default, `useRangeCalendar` will emit `CalendarDate` objects in the `onChange` event, but if a` CalendarDateTime `or` ZonedDateTime `object is passed as the `value` or `defaultValue`, values of that type will be emitted, changing only the date and preserving the time components. import {parseDate} from '@internationalized/date'; function Example() { let [value, setValue] = React.useState({ start: parseDate('2020-02-03'), end: parseDate('2020-02-12') }); return ( <div style={{display: 'flex', gap: 20, flexWrap: 'wrap'}}> <RangeCalendar aria-label="Date range (uncontrolled)" defaultValue={{ start: parseDate('2020-02-03'), end: parseDate('2020-02-12') }} /> <RangeCalendar aria-label="Date range (controlled)" value={value} onChange={setValue} /> </div> ); } import {parseDate} from '@internationalized/date'; function Example() { let [value, setValue] = React.useState({ start: parseDate('2020-02-03'), end: parseDate('2020-02-12') }); return ( <div style={{ display: 'flex', gap: 20, flexWrap: 'wrap' }} > <RangeCalendar aria-label="Date range (uncontrolled)" defaultValue={{ start: parseDate('2020-02-03'), end: parseDate('2020-02-12') }} /> <RangeCalendar aria-label="Date range (controlled)" value={value} onChange={setValue} /> </div> ); } import {parseDate} from '@internationalized/date'; function Example() { let [value, setValue] = React.useState({ start: parseDate( '2020-02-03' ), end: parseDate( '2020-02-12' ) }); return ( <div style={{ display: 'flex', gap: 20, flexWrap: 'wrap' }} > <RangeCalendar aria-label="Date range (uncontrolled)" defaultValue={{ start: parseDate( '2020-02-03' ), end: parseDate( '2020-02-12' ) }} /> <RangeCalendar aria-label="Date range (controlled)" value={value} onChange={setValue} /> </div> ); } ## February 2020 <\> | S | M | T | W | T | F | S | | --- | --- | --- | --- | --- | --- | --- | | 26 | 27 | 28 | 29 | 30 | 31 | 1 | | 2 | 3 | 4 | 5 | 6 | 7 | 8 | | 9 | 10 | 11 | 12 | 13 | 14 | 15 | | 16 | 17 | 18 | 19 | 20 | 21 | 22 | | 23 | 24 | 25 | 26 | 27 | 28 | 29 | ## February 2020 <\> | S | M | T | W | T | F | S | | --- | --- | --- | --- | --- | --- | --- | | 26 | 27 | 28 | 29 | 30 | 31 | 1 | | 2 | 3 | 4 | 5 | 6 | 7 | 8 | | 9 | 10 | 11 | 12 | 13 | 14 | 15 | | 16 | 17 | 18 | 19 | 20 | 21 | 22 | | 23 | 24 | 25 | 26 | 27 | 28 | 29 | ### Events# `useRangeCalendar` accepts an `onChange` prop which is triggered whenever a date is selected by the user. The example below uses `onChange` to update a separate element with a formatted version of the date in the user's locale. This is done by converting the date to a native JavaScript `Date` object to pass to the formatter. import {useDateFormatter} from 'react-aria'; import {getLocalTimeZone} from '@internationalized/date'; function Example() { let [range, setRange] = React.useState({ start: parseDate('2020-07-03'), end: parseDate('2020-07-10') }); let formatter = useDateFormatter({ dateStyle: 'long' }); return ( <> <RangeCalendar aria-label="Date range" value={range} onChange={setRange} /> <p> Selected date: {formatter.formatRange( range.start.toDate(getLocalTimeZone()), range.end.toDate(getLocalTimeZone()) )} </p> </> ); } import {useDateFormatter} from 'react-aria'; import {getLocalTimeZone} from '@internationalized/date'; function Example() { let [range, setRange] = React.useState({ start: parseDate('2020-07-03'), end: parseDate('2020-07-10') }); let formatter = useDateFormatter({ dateStyle: 'long' }); return ( <> <RangeCalendar aria-label="Date range" value={range} onChange={setRange} /> <p> Selected date: {formatter.formatRange( range.start.toDate(getLocalTimeZone()), range.end.toDate(getLocalTimeZone()) )} </p> </> ); } import {useDateFormatter} from 'react-aria'; import {getLocalTimeZone} from '@internationalized/date'; function Example() { let [range, setRange] = React.useState({ start: parseDate( '2020-07-03' ), end: parseDate( '2020-07-10' ) }); let formatter = useDateFormatter({ dateStyle: 'long' }); return ( <> <RangeCalendar aria-label="Date range" value={range} onChange={setRange} /> <p> Selected date: {' '} {formatter .formatRange( range.start .toDate( getLocalTimeZone() ), range.end .toDate( getLocalTimeZone() ) )} </p> </> ); } ## July 2020 <\> | S | M | T | W | T | F | S | | --- | --- | --- | --- | --- | --- | --- | | 28 | 29 | 30 | 1 | 2 | 3 | 4 | | 5 | 6 | 7 | 8 | 9 | 10 | 11 | | 12 | 13 | 14 | 15 | 16 | 17 | 18 | | 19 | 20 | 21 | 22 | 23 | 24 | 25 | | 26 | 27 | 28 | 29 | 30 | 31 | 1 | Selected date: July 3 – 10, 2020 ### International calendars# `useRangeCalendar` supports selecting dates in many calendar systems used around the world, including Gregorian, Hebrew, Indian, Islamic, Buddhist, and more. Dates are automatically displayed in the appropriate calendar system for the user's locale. The calendar system can be overridden using the Unicode calendar locale extension, passed to the I18nProvider component. Selected dates passed to `onChange` always use the same calendar system as the `value` or `defaultValue` prop. If no `value` or `defaultValue` is provided, then dates passed to `onChange` are always in the Gregorian calendar since this is the most commonly used. This means that even though the user selects dates in their local calendar system, applications are able to deal with dates from all users consistently. The below example displays a `RangeCalendar` in the Hindi language, using the Indian calendar. Dates emitted from `onChange` are in the Gregorian calendar. import {I18nProvider} from 'react-aria'; function Example() { let [range, setRange] = React.useState(null); return ( <I18nProvider locale="hi-IN-u-ca-indian"> <RangeCalendar aria-label="Date range" value={range} onChange={setRange} /> <p>Start date: {range?.start.toString()}</p> <p>End date: {range?.end.toString()}</p> </I18nProvider> ); } import {I18nProvider} from 'react-aria'; function Example() { let [range, setRange] = React.useState(null); return ( <I18nProvider locale="hi-IN-u-ca-indian"> <RangeCalendar aria-label="Date range" value={range} onChange={setRange} /> <p>Start date: {range?.start.toString()}</p> <p>End date: {range?.end.toString()}</p> </I18nProvider> ); } import {I18nProvider} from 'react-aria'; function Example() { let [range, setRange] = React.useState(null); return ( <I18nProvider locale="hi-IN-u-ca-indian"> <RangeCalendar aria-label="Date range" value={range} onChange={setRange} /> <p> Start date:{' '} {range?.start .toString()} </p> <p> End date:{' '} {range?.end .toString()} </p> </I18nProvider> ); } ## शक 1947 ज्येष्ठ <\> | र | सो | मं | बु | गु | शु | श | | --- | --- | --- | --- | --- | --- | --- | | 28 | 29 | 30 | 31 | 1 | 2 | 3 | | 4 | 5 | 6 | 7 | 8 | 9 | 10 | | 11 | 12 | 13 | 14 | 15 | 16 | 17 | | 18 | 19 | 20 | 21 | 22 | 23 | 24 | | 25 | 26 | 27 | 28 | 29 | 30 | 31 | Start date: End date: ### Custom calendar systems# `RangeCalendar` also supports custom calendar systems that implement custom business rules. An example would be a fiscal year calendar that follows a 4-5-4 format, where month ranges don't follow the usual Gregorian calendar. The `createCalendar` prop accepts a function that returns an instance of the `Calendar` interface. See the @internationalized/date docs for an example implementation. import {GregorianCalendar} from '@internationalized/date'; function Example() { return ( <RangeCalendar firstDayOfWeek="sun" createCalendar={() => new Custom454()} /> ); } class Custom454 extends GregorianCalendar { // See @internationalized/date docs linked above... } import {GregorianCalendar} from '@internationalized/date'; function Example() { return ( <RangeCalendar firstDayOfWeek="sun" createCalendar={() => new Custom454()} /> ); } class Custom454 extends GregorianCalendar { // See @internationalized/date docs linked above... } import {GregorianCalendar} from '@internationalized/date'; function Example() { return ( <RangeCalendar firstDayOfWeek="sun" createCalendar={() => new Custom454()} /> ); } class Custom454 extends GregorianCalendar { // See @internationalized/date docs linked above... } ## June 2025 <\> | S | M | T | W | T | F | S | | --- | --- | --- | --- | --- | --- | --- | | 1 | 2 | 3 | 4 | 5 | 6 | 7 | | 8 | 9 | 10 | 11 | 12 | 13 | 14 | | 15 | 16 | 17 | 18 | 19 | 20 | 21 | | 22 | 23 | 24 | 25 | 26 | 27 | 28 | ### Validation# By default, `useRangeCalendar` allows selecting any date range. The `minValue` and `maxValue` props can also be used to prevent the user from selecting dates outside a certain range. This example only accepts dates after today. import {today} from '@internationalized/date'; <RangeCalendar aria-label="Trip dates" minValue={today(getLocalTimeZone())} /> import {today} from '@internationalized/date'; <RangeCalendar aria-label="Trip dates" minValue={today(getLocalTimeZone())} /> import {today} from '@internationalized/date'; <RangeCalendar aria-label="Trip dates" minValue={today( getLocalTimeZone() )} /> ## June 2025 <\> | S | M | T | W | T | F | S | | --- | --- | --- | --- | --- | --- | --- | | 1 | 2 | 3 | 4 | 5 | 6 | 7 | | 8 | 9 | 10 | 11 | 12 | 13 | 14 | | 15 | 16 | 17 | 18 | 19 | 20 | 21 | | 22 | 23 | 24 | 25 | 26 | 27 | 28 | | 29 | 30 | 1 | 2 | 3 | 4 | 5 | ### Unavailable dates# `useRangeCalendar` supports marking certain dates as _unavailable_. These dates remain focusable with the keyboard so that navigation is consistent, but cannot be selected by the user. In this example, they are displayed in red. The `isDateUnavailable` prop accepts a callback that is called to evaluate whether each visible date is unavailable. Note that by default, users may not select non-contiguous ranges, i.e. ranges that contain unavailable dates within them. Once a start date is selected, enabled dates will be restricted to subsequent dates until an unavailable date is hit. See below for an example of how to allow non-contiguous ranges. This example includes multiple unavailable date ranges, e.g. dates when a rental house is not available. The `minValue` prop is also used to prevent selecting dates before today. import {today} from '@internationalized/date'; function Example() { let now = today(getLocalTimeZone()); let disabledRanges = [ [now, now.add({ days: 5 })], [now.add({ days: 14 }), now.add({ days: 16 })], [now.add({ days: 23 }), now.add({ days: 24 })] ]; let isDateUnavailable = (date) => disabledRanges.some((interval) => date.compare(interval[0]) >= 0 && date.compare(interval[1]) <= 0 ); return ( <RangeCalendar aria-label="Trip dates" minValue={today(getLocalTimeZone())} isDateUnavailable={isDateUnavailable} /> ); } import {today} from '@internationalized/date'; function Example() { let now = today(getLocalTimeZone()); let disabledRanges = [ [now, now.add({ days: 5 })], [now.add({ days: 14 }), now.add({ days: 16 })], [now.add({ days: 23 }), now.add({ days: 24 })] ]; let isDateUnavailable = (date) => disabledRanges.some((interval) => date.compare(interval[0]) >= 0 && date.compare(interval[1]) <= 0 ); return ( <RangeCalendar aria-label="Trip dates" minValue={today(getLocalTimeZone())} isDateUnavailable={isDateUnavailable} /> ); } import {today} from '@internationalized/date'; function Example() { let now = today( getLocalTimeZone() ); let disabledRanges = [ [ now, now.add({ days: 5 }) ], [ now.add({ days: 14 }), now.add({ days: 16 }) ], [ now.add({ days: 23 }), now.add({ days: 24 }) ] ]; let isDateUnavailable = (date) => disabledRanges .some(( interval ) => date.compare( interval[0] ) >= 0 && date.compare( interval[1] ) <= 0 ); return ( <RangeCalendar aria-label="Trip dates" minValue={today( getLocalTimeZone() )} isDateUnavailable={isDateUnavailable} /> ); } ## June 2025 <\> | S | M | T | W | T | F | S | | --- | --- | --- | --- | --- | --- | --- | | 1 | 2 | 3 | 4 | 5 | 6 | 7 | | 8 | 9 | 10 | 11 | 12 | 13 | 14 | | 15 | 16 | 17 | 18 | 19 | 20 | 21 | | 22 | 23 | 24 | 25 | 26 | 27 | 28 | | 29 | 30 | 1 | 2 | 3 | 4 | 5 | ### Non-contiguous ranges# The `allowsNonContiguousRanges` prop enables a range to be selected even if there are unavailable dates in the middle. The value emitted in the `onChange` event will still be a single range with a `start` and `end` property, but unavailable dates will not be displayed as selected. It is up to applications to split the full selected range into multiple as needed for business logic. This example prevents selecting weekends, but allows selecting ranges that span multiple weeks. import {isWeekend} from '@internationalized/date'; function Example() { let { locale } = useLocale(); return ( <RangeCalendar aria-label="Time off request" isDateUnavailable={(date) => isWeekend(date, locale)} allowsNonContiguousRanges /> ); } import {isWeekend} from '@internationalized/date'; function Example() { let { locale } = useLocale(); return ( <RangeCalendar aria-label="Time off request" isDateUnavailable={(date) => isWeekend(date, locale)} allowsNonContiguousRanges /> ); } import {isWeekend} from '@internationalized/date'; function Example() { let { locale } = useLocale(); return ( <RangeCalendar aria-label="Time off request" isDateUnavailable={(date) => isWeekend( date, locale )} allowsNonContiguousRanges /> ); } ## June 2025 <\> | S | M | T | W | T | F | S | | --- | --- | --- | --- | --- | --- | --- | | 1 | 2 | 3 | 4 | 5 | 6 | 7 | | 8 | 9 | 10 | 11 | 12 | 13 | 14 | | 15 | 16 | 17 | 18 | 19 | 20 | 21 | | 22 | 23 | 24 | 25 | 26 | 27 | 28 | | 29 | 30 | 1 | 2 | 3 | 4 | 5 | ### Controlling the focused date# By default, the first selected date is focused when a `RangeCalendar` first mounts. If no `value` or `defaultValue` prop is provided, then the current date is focused. However, `useRangeCalendar` supports controlling which date is focused using the `focusedValue` and `onFocusChange` props. This also determines which month is visible. The `defaultFocusedValue` prop allows setting the initial focused date when the `RangeCalendar` first mounts, without controlling it. This example focuses July 1, 2021 by default. The user may change the focused date, and the `onFocusChange` event updates the state. Clicking the button resets the focused date back to the initial value. import {CalendarDate} from '@internationalized/date'; function Example() { let defaultDate = new CalendarDate(2021, 7, 1); let [focusedDate, setFocusedDate] = React.useState(defaultDate); return ( <div style={{ flexDirection: 'column', alignItems: 'start', gap: 20 }}> <button onClick={() => setFocusedDate(defaultDate)}> Reset focused date </button> <RangeCalendar focusedValue={focusedDate} onFocusChange={setFocusedDate} /> </div> ); } import {CalendarDate} from '@internationalized/date'; function Example() { let defaultDate = new CalendarDate(2021, 7, 1); let [focusedDate, setFocusedDate] = React.useState( defaultDate ); return ( <div style={{ flexDirection: 'column', alignItems: 'start', gap: 20 }} > <button onClick={() => setFocusedDate(defaultDate)}> Reset focused date </button> <RangeCalendar focusedValue={focusedDate} onFocusChange={setFocusedDate} /> </div> ); } import {CalendarDate} from '@internationalized/date'; function Example() { let defaultDate = new CalendarDate( 2021, 7, 1 ); let [ focusedDate, setFocusedDate ] = React.useState( defaultDate ); return ( <div style={{ flexDirection: 'column', alignItems: 'start', gap: 20 }} > <button onClick={() => setFocusedDate( defaultDate )} > Reset focused date </button> <RangeCalendar focusedValue={focusedDate} onFocusChange={setFocusedDate} /> </div> ); } Reset focused date ## July 2021 <\> | S | M | T | W | T | F | S | | --- | --- | --- | --- | --- | --- | --- | | 27 | 28 | 29 | 30 | 1 | 2 | 3 | | 4 | 5 | 6 | 7 | 8 | 9 | 10 | | 11 | 12 | 13 | 14 | 15 | 16 | 17 | | 18 | 19 | 20 | 21 | 22 | 23 | 24 | | 25 | 26 | 27 | 28 | 29 | 30 | 31 | ### Disabled# The `isDisabled` boolean prop makes the RangeCalendar disabled. Cells cannot be focused or selected. <RangeCalendar aria-label="Trip dates" isDisabled /> <RangeCalendar aria-label="Trip dates" isDisabled /> <RangeCalendar aria-label="Trip dates" isDisabled /> ## June 2025 <\> | S | M | T | W | T | F | S | | --- | --- | --- | --- | --- | --- | --- | | 1 | 2 | 3 | 4 | 5 | 6 | 7 | | 8 | 9 | 10 | 11 | 12 | 13 | 14 | | 15 | 16 | 17 | 18 | 19 | 20 | 21 | | 22 | 23 | 24 | 25 | 26 | 27 | 28 | | 29 | 30 | 1 | 2 | 3 | 4 | 5 | ### Read only# The `isReadOnly` boolean prop makes the RangeCalendar's value immutable. Unlike `isDisabled`, the RangeCalendar remains focusable. <RangeCalendar aria-label="Trip dates" value={{ start: today(getLocalTimeZone()), end: today(getLocalTimeZone()).add({ weeks: 1 }) }} isReadOnly /> <RangeCalendar aria-label="Trip dates" value={{ start: today(getLocalTimeZone()), end: today(getLocalTimeZone()).add({ weeks: 1 }) }} isReadOnly /> <RangeCalendar aria-label="Trip dates" value={{ start: today( getLocalTimeZone() ), end: today( getLocalTimeZone() ).add({ weeks: 1 }) }} isReadOnly /> ## June 2025 <\> | S | M | T | W | T | F | S | | --- | --- | --- | --- | --- | --- | --- | | 1 | 2 | 3 | 4 | 5 | 6 | 7 | | 8 | 9 | 10 | 11 | 12 | 13 | 14 | | 15 | 16 | 17 | 18 | 19 | 20 | 21 | | 22 | 23 | 24 | 25 | 26 | 27 | 28 | | 29 | 30 | 1 | 2 | 3 | 4 | 5 | ### Custom first day of week# By default, the first day of the week is automatically set based on the current locale. This can be changed by setting the `firstDayOfWeek` prop to `'sun'`, `'mon'`, `'tue'`, `'wed'`, `'thu'`, `'fri'`, or `'sat'`. <RangeCalendar aria-label="Trip dates" firstDayOfWeek="mon" /> <RangeCalendar aria-label="Trip dates" firstDayOfWeek="mon" /> <RangeCalendar aria-label="Trip dates" firstDayOfWeek="mon" /> ## June 2025 <\> | M | T | W | T | F | S | S | | --- | --- | --- | --- | --- | --- | --- | | 26 | 27 | 28 | 29 | 30 | 31 | 1 | | 2 | 3 | 4 | 5 | 6 | 7 | 8 | | 9 | 10 | 11 | 12 | 13 | 14 | 15 | | 16 | 17 | 18 | 19 | 20 | 21 | 22 | | 23 | 24 | 25 | 26 | 27 | 28 | 29 | | 30 | 1 | 2 | 3 | 4 | 5 | 6 | ### Labeling# An aria-label must be provided to the `RangeCalendar` for accessibility. If it is labeled by a separate element, an `aria-labelledby` prop must be provided using the `id` of the labeling element instead. ### Internationalization# In order to internationalize a `RangeCalendar`, a localized string should be passed to the `aria-label` prop. For languages that are read right-to-left (e.g. Hebrew and Arabic), keyboard navigation is automatically flipped. Ensure that your CSS accounts for this as well. Dates are automatically formatted using the current locale. ## Advanced topics# * * * ### Reducing bundle size# In the example above, the `createCalendar` function from the @internationalized/date package is passed to the` useRangeCalendarState `hook. This function receives a calendar identifier string, and provides` Calendar `instances to React Stately, which are used to implement date manipulation. By default, this includes all calendar systems supported by `@internationalized/date`. However, if your application supports a more limited set of regions, or you know you will only be picking dates in a certain calendar system, you can reduce your bundle size by providing your own implementation of `createCalendar` that includes a subset of these `Calendar` implementations. For example, if your application only supports Gregorian dates, you could implement a `createCalendar` function like this: import {useLocale} from 'react-aria'; import {useRangeCalendarState} from 'react-stately'; import {GregorianCalendar} from '@internationalized/date'; function createCalendar(identifier) { switch (identifier) { case 'gregory': return new GregorianCalendar(); default: throw new Error(`Unsupported calendar ${identifier}`); } } function RangeCalendar(props) { let { locale } = useLocale(); let state = useRangeCalendarState({ ...props, locale, createCalendar }); // ... } import {useLocale} from 'react-aria'; import {useRangeCalendarState} from 'react-stately'; import {GregorianCalendar} from '@internationalized/date'; function createCalendar(identifier) { switch (identifier) { case 'gregory': return new GregorianCalendar(); default: throw new Error(`Unsupported calendar ${identifier}`); } } function RangeCalendar(props) { let { locale } = useLocale(); let state = useRangeCalendarState({ ...props, locale, createCalendar }); // ... } import {useLocale} from 'react-aria'; import {useRangeCalendarState} from 'react-stately'; import {GregorianCalendar} from '@internationalized/date'; function createCalendar( identifier ) { switch (identifier) { case 'gregory': return new GregorianCalendar(); default: throw new Error( `Unsupported calendar ${identifier}` ); } } function RangeCalendar( props ) { let { locale } = useLocale(); let state = useRangeCalendarState( { ...props, locale, createCalendar } ); // ... } This way, only `GregorianCalendar` is imported, and the other calendar implementations can be tree-shaken. See the Calendar documentation in `@internationalized/date` to learn more about the supported calendar systems, and a list of string identifiers. --- ## Page: https://react-spectrum.adobe.com/react-aria/useTimeField.html # useTimeField Provides the behavior and accessibility implementation for a time field component. A time field allows users to enter and edit time values using a keyboard. Each part of a time value is displayed in an individually editable segment. <table><tbody><tr><th>install</th><td><code>yarn add react-aria</code></td></tr><tr><th>version</th><td>3.41.1</td></tr><tr><th>usage</th><td><code><span>import</span> {useTimeField} <span>from</span> <span>'react-aria'</span></code></td></tr></tbody></table> View repository GitHub View package NPM ## API# * * * `useTimeField<T extends TimeValue >( props: AriaTimeFieldOptions <T>, state: TimeFieldState , ref: RefObject <Element | | null> ): DateFieldAria ``useDateSegment( segment: DateSegment , state: DateFieldState , ref: RefObject <HTMLElement | | null> ): DateSegmentAria ` ## Features# * * * A time field can be built using `<input type="time">`, but this is very limited in functionality, lacking in internationalization capabilities, inconsistent between browsers, and difficult to style. `useTimeField` helps achieve accessible and international time fields that can be styled as needed. * **International** – Support for locale-specific formatting, number systems, hour cycles, and right-to-left layout. * **Time zone aware** – Times can optionally include a time zone. All modifications follow time zone rules such as daylight saving time. * **Accessible** – Each time unit is displayed as an individually focusable and editable segment, which allows users an easy way to edit times using the keyboard, in any format and locale. * **Touch friendly** – Time segments are editable using an easy to use numeric keypad, and all interactions are accessible using touch-based screen readers. * **Customizable** – As with all of React Aria, the DOM structure and styling of all elements can be fully customized. Read our blog post for more details about the internationalization, accessibility, and user experience features implemented by `useTimeField`. ## Anatomy# * * * A time field consists of a label, and a group of segments representing each unit of a time (e.g. hours, minutes, and seconds). Each segment is individually focusable and editable by the user, by typing or using the arrow keys to increment and decrement the value. This approach allows values to be formatted and parsed correctly regardless of the locale or time format, and offers an easy and error-free way to edit times using the keyboard. `useTimeField` also supports optional description and error message elements, which can be used to provide more context about the field, and any validation messages. These are linked with the input via the `aria-describedby` attribute. `useTimeField` returns props that you should spread onto the appropriate elements: | Name | Type | Description | | --- | --- | --- | | `labelProps` | `DOMAttributes` | Props for the field's visible label element, if any. | | `fieldProps` | ` GroupDOMAttributes ` | Props for the field grouping element. | | `inputProps` | `InputHTMLAttributes<HTMLInputElement>` | Props for the hidden input element for HTML form submission. | | `descriptionProps` | `DOMAttributes` | Props for the description element, if any. | | `errorMessageProps` | `DOMAttributes` | Props for the error message element, if any. | | `isInvalid` | `boolean` | Whether the input value is invalid. | | `validationErrors` | `string[]` | The current error messages for the input if it is invalid, otherwise an empty array. | | `validationDetails` | `ValidityState` | The native validation details for the input. | `useDateSegment` returns props for an individual time segment: | Name | Type | Description | | --- | --- | --- | | `segmentProps` | `React.HTMLAttributes<HTMLDivElement>` | Props for the segment element. | Note that most of this anatomy is shared with useDateField, so you can reuse many components between them if you have both. State is managed by the `useTimeFieldState` hook from `@react-stately/datepicker`. The state object should be passed as an option to `useTimeField` and `useDateSegment`. If the time field does not have a visible label, an `aria-label` or `aria-labelledby` prop must be passed instead to identify it to assistive technology. ## Time values# * * * Times are represented in many different ways by cultures around the world. This includes differences in hour cycles, time zones, daylight saving time rules, formatting, and much more. When building applications that support users around the world, it is important to handle these aspects correctly for each locale. `useTimeField` uses the @internationalized/date library to represent times. This package provides a library of objects and functions to perform date and time related manipulation, queries, and conversions that work across locales and calendars. Date and time objects can be converted to and from native JavaScript `Date` objects or ISO 8601 strings. See the documentation, or the examples below for more details. ## Example# * * * import {useDateSegment, useLocale, useTimeField} from 'react-aria'; import {useTimeFieldState} from 'react-stately'; export function TimeField(props) { let { locale } = useLocale(); let state = useTimeFieldState({ ...props, locale }); let ref = React.useRef(null); let { labelProps, fieldProps } = useTimeField(props, state, ref); return ( <div className="wrapper"> <span {...labelProps}>{props.label}</span> <div {...fieldProps} ref={ref} className="field"> {state.segments.map((segment, i) => ( <DateSegment key={i} segment={segment} state={state} /> ))} {state.isInvalid && <span aria-hidden="true">🚫</span>} </div> </div> ); } // Note: this component is the same as in the useDateField docs. function DateSegment({ segment, state }) { let ref = React.useRef(null); let { segmentProps } = useDateSegment(segment, state, ref); return ( <div {...segmentProps} ref={ref} className={`segment ${segment.isPlaceholder ? 'placeholder' : ''}`} > {segment.text} </div> ); } <TimeField label="Event date" /> import { useDateSegment, useLocale, useTimeField } from 'react-aria'; import {useTimeFieldState} from 'react-stately'; export function TimeField(props) { let { locale } = useLocale(); let state = useTimeFieldState({ ...props, locale }); let ref = React.useRef(null); let { labelProps, fieldProps } = useTimeField( props, state, ref ); return ( <div className="wrapper"> <span {...labelProps}>{props.label}</span> <div {...fieldProps} ref={ref} className="field"> {state.segments.map((segment, i) => ( <DateSegment key={i} segment={segment} state={state} /> ))} {state.isInvalid && <span aria-hidden="true">🚫</span>} </div> </div> ); } // Note: this component is the same as in the useDateField docs. function DateSegment({ segment, state }) { let ref = React.useRef(null); let { segmentProps } = useDateSegment( segment, state, ref ); return ( <div {...segmentProps} ref={ref} className={`segment ${ segment.isPlaceholder ? 'placeholder' : '' }`} > {segment.text} </div> ); } <TimeField label="Event date" /> import { useDateSegment, useLocale, useTimeField } from 'react-aria'; import {useTimeFieldState} from 'react-stately'; export function TimeField( props ) { let { locale } = useLocale(); let state = useTimeFieldState({ ...props, locale }); let ref = React.useRef( null ); let { labelProps, fieldProps } = useTimeField( props, state, ref ); return ( <div className="wrapper"> <span {...labelProps} > {props.label} </span> <div {...fieldProps} ref={ref} className="field" > {state.segments .map(( segment, i ) => ( <DateSegment key={i} segment={segment} state={state} /> ))} {state .isInvalid && ( <span aria-hidden="true"> 🚫 </span> )} </div> </div> ); } // Note: this component is the same as in the useDateField docs. function DateSegment( { segment, state } ) { let ref = React.useRef( null ); let { segmentProps } = useDateSegment( segment, state, ref ); return ( <div {...segmentProps} ref={ref} className={`segment ${ segment .isPlaceholder ? 'placeholder' : '' }`} > {segment.text} </div> ); } <TimeField label="Event date" /> Event date –– : –– AM Show CSS .wrapper { display: flex; flex-direction: column; align-items: flex-start; } .field { display: inline-flex; padding: 2px 4px; border-radius: 2px; border: 1px solid var(--gray); background: var(--spectrum-global-color-gray-50); } .field:focus-within { border-color: var(--blue); } .segment { padding: 0 2px; font-variant-numeric: tabular-nums; text-align: end; } .segment.placeholder { color: var(--spectrum-gray-600); } .segment:focus { color: white; background: var(--blue); outline: none; border-radius: 2px; } .wrapper { display: flex; flex-direction: column; align-items: flex-start; } .field { display: inline-flex; padding: 2px 4px; border-radius: 2px; border: 1px solid var(--gray); background: var(--spectrum-global-color-gray-50); } .field:focus-within { border-color: var(--blue); } .segment { padding: 0 2px; font-variant-numeric: tabular-nums; text-align: end; } .segment.placeholder { color: var(--spectrum-gray-600); } .segment:focus { color: white; background: var(--blue); outline: none; border-radius: 2px; } .wrapper { display: flex; flex-direction: column; align-items: flex-start; } .field { display: inline-flex; padding: 2px 4px; border-radius: 2px; border: 1px solid var(--gray); background: var(--spectrum-global-color-gray-50); } .field:focus-within { border-color: var(--blue); } .segment { padding: 0 2px; font-variant-numeric: tabular-nums; text-align: end; } .segment.placeholder { color: var(--spectrum-gray-600); } .segment:focus { color: white; background: var(--blue); outline: none; border-radius: 2px; } ## Styled examples# * * * Tailwind CSS A time field built with Tailwind and React Aria. ## Usage# * * * The following examples show how to use the `TimeField` component created in the above example. ### Value# A `TimeField` displays a placeholder by default. An initial, uncontrolled value can be provided to the `TimeField` using the `defaultValue` prop. Alternatively, a controlled value can be provided using the `value` prop. Time values are provided using objects in the @internationalized/date package. This library handles correct international date and time manipulation across calendars, time zones, and other localization concerns. `useTimeField` only supports selecting times, but values with date components are also accepted. By default, `useTimeField` will emit `Time` objects in the `onChange` event, but if a` CalendarDateTime `or` ZonedDateTime `object is passed as the `value` or `defaultValue`, values of that type will be emitted, changing only the time and preserving the date components. import {Time} from '@internationalized/date'; function Example() { let [value, setValue] = React.useState(new Time(11, 45)); return ( <> <TimeField label="Time (uncontrolled)" defaultValue={new Time(11, 45)} /> <TimeField label="Time (controlled)" value={value} onChange={setValue} /> </> ); } import {Time} from '@internationalized/date'; function Example() { let [value, setValue] = React.useState(new Time(11, 45)); return ( <> <TimeField label="Time (uncontrolled)" defaultValue={new Time(11, 45)} /> <TimeField label="Time (controlled)" value={value} onChange={setValue} /> </> ); } import {Time} from '@internationalized/date'; function Example() { let [value, setValue] = React.useState( new Time(11, 45) ); return ( <> <TimeField label="Time (uncontrolled)" defaultValue={new Time( 11, 45 )} /> <TimeField label="Time (controlled)" value={value} onChange={setValue} /> </> ); } Time (uncontrolled) 11 : 45 AM Time (controlled) 11 : 45 AM `Time` values may also be parsed from strings using the `parseTime` function. This accepts ISO 8601 formatted time strings such as `"04:45:23.123"`. The `toString` method of a `Time` object can also be used to convert a time object to a string. ### Time zones# `useTimeField` is time zone aware when a `ZonedDateTime` object is provided as the value. In this case, the time zone abbreviation is displayed, and time zone concerns such as daylight saving time are taken into account when the value is manipulated. In most cases, your data will come from and be sent to a server as an ISO 8601 formatted string. @internationalized/date includes functions for parsing strings in multiple formats into `ZonedDateTime` objects. Which format you use will depend on what information you need to store. * ` parseZonedDateTime `– This function parses a date with an explicit time zone and optional UTC offset attached (e.g. `"2021-11-07T00:45[America/Los_Angeles]"` or `"2021-11-07T00:45-07:00[America/Los_Angeles]"`). This format preserves the maximum amount of information. If the exact local time and time zone that a user selected is important, use this format. Storing the time zone and offset that was selected rather than converting to UTC ensures that the local time is correct regardless of daylight saving rule changes (e.g. if a locale abolishes DST). Examples where this applies include calendar events, reminders, and other times that occur in a particular location. * ` parseAbsolute `– This function parses an absolute date and time that occurs at the same instant at all locations on Earth. It can be represented in UTC (e.g. `"2021-11-07T07:45:00Z"`), or stored with a particular offset (e.g. `"2021-11-07T07:45:00-07:00"`). A time zone identifier, e.g. `America/Los_Angeles`, must be passed, and the result will be converted into that time zone. Absolute times are the best way to represent events that occurred in the past, or future events where an exact time is needed, regardless of time zone. * ` parseAbsoluteToLocal `– This function parses an absolute date and time into the current user's local time zone. It is a shortcut for `parseAbsolute`, and accepts the same formats. import {parseZonedDateTime} from '@internationalized/date'; <TimeField label="Event time" defaultValue={parseZonedDateTime('2022-11-07T00:45[America/Los_Angeles]')} /> import {parseZonedDateTime} from '@internationalized/date'; <TimeField label="Event time" defaultValue={parseZonedDateTime( '2022-11-07T00:45[America/Los_Angeles]' )} /> import {parseZonedDateTime} from '@internationalized/date'; <TimeField label="Event time" defaultValue={parseZonedDateTime( '2022-11-07T00:45[America/Los_Angeles]' )} /> Event time 12 : 45 AM PST `useTimeField` displays times in the time zone included in the `ZonedDateTime` object. The above example is always displayed in Pacific Standard Time because the `America/Los_Angeles` time zone identifier is provided. @internationalized/date includes functions for converting dates between time zones, or parsing a date directly into a specific time zone or the user's local time zone, as shown below. import {parseAbsoluteToLocal} from '@internationalized/date'; <TimeField label="Event time" defaultValue={parseAbsoluteToLocal('2021-11-07T07:45:00Z')} /> import {parseAbsoluteToLocal} from '@internationalized/date'; <TimeField label="Event time" defaultValue={parseAbsoluteToLocal( '2021-11-07T07:45:00Z' )} /> import {parseAbsoluteToLocal} from '@internationalized/date'; <TimeField label="Event time" defaultValue={parseAbsoluteToLocal( '2021-11-07T07:45:00Z' )} /> Event time 7 : 45 AM UTC ### Events# `useTimeField` accepts an `onChange` prop which is triggered whenever the time is edited by the user. The example below uses `onChange` to update a separate element with a formatted version of the date in the user's locale and local time zone. This is done by converting the date to a native JavaScript `Date` object to pass to the formatter. `useTimeField` allows editing the time components while keeping the date fixed. import {useDateFormatter} from 'react-aria'; function Example() { let [date, setDate] = React.useState( parseAbsoluteToLocal('2021-04-07T18:45:22Z') ); let formatter = useDateFormatter({ dateStyle: 'long', timeStyle: 'long' }); return ( <> <TimeField label="Time" value={date} onChange={setDate} /> <p> Selected date and time:{' '} {(date?.toDate && formatter.format(date.toDate())) || (date && date.toString()) || '--'} </p> </> ); } import {useDateFormatter} from 'react-aria'; function Example() { let [date, setDate] = React.useState( parseAbsoluteToLocal('2021-04-07T18:45:22Z') ); let formatter = useDateFormatter({ dateStyle: 'long', timeStyle: 'long' }); return ( <> <TimeField label="Time" value={date} onChange={setDate} /> <p> Selected date and time:{' '} {(date?.toDate && formatter.format(date.toDate())) || (date && date.toString()) || '--'} </p> </> ); } import {useDateFormatter} from 'react-aria'; function Example() { let [date, setDate] = React.useState( parseAbsoluteToLocal( '2021-04-07T18:45:22Z' ) ); let formatter = useDateFormatter({ dateStyle: 'long', timeStyle: 'long' }); return ( <> <TimeField label="Time" value={date} onChange={setDate} /> <p> Selected date and time:{' '} {(date?.toDate && formatter .format( date .toDate() )) || (date && date .toString()) || '--'} </p> </> ); } Time 6 : 45 PM UTC Selected date and time: April 7, 2021 at 6:45:22 PM UTC ### Granularity# The `granularity` prop allows you to control the smallest unit that is displayed by `useTimeField`. By default, times are displayed with `"minute"` granularity. More granular time values can be displayed by setting the `granularity` prop to `"second"`. <TimeField label="Event time" granularity="second" defaultValue={parseAbsoluteToLocal('2021-04-07T18:45:22Z')} /> <TimeField label="Event time" granularity="second" defaultValue={parseAbsoluteToLocal( '2021-04-07T18:45:22Z' )} /> <TimeField label="Event time" granularity="second" defaultValue={parseAbsoluteToLocal( '2021-04-07T18:45:22Z' )} /> Event time 6 : 45 : 22 PM UTC ### Minimum and maximum values# The `minValue` and `maxValue` props can also be used to perform builtin validation. This marks the time field as invalid using ARIA if the user enters an invalid time. You should implement a visual indication that the time field is invalid as well. This example only accepts times between 9 AM and 5 PM. <TimeField label="Meeting time" minValue={new Time(9)} maxValue={new Time(17)} defaultValue={new Time(8)} /> <TimeField label="Meeting time" minValue={new Time(9)} maxValue={new Time(17)} defaultValue={new Time(8)} /> <TimeField label="Meeting time" minValue={new Time( 9 )} maxValue={new Time( 17 )} defaultValue={new Time( 8 )} /> Meeting time 8 : 00 AM 🚫 ### Placeholder value# When no value is set, a placeholder is shown. The format of the placeholder is influenced by the `granularity` and `placeholderValue` props. `placeholderValue` also controls the default values of each segment when the user first interacts with them, e.g. using the up and down arrow keys. By default, the `placeholderValue` is midnight, but you can set it to a more appropriate value if needed. <TimeField label="Meeting time" placeholderValue={new Time(9)} /> <TimeField label="Meeting time" placeholderValue={new Time(9)} /> <TimeField label="Meeting time" placeholderValue={new Time( 9 )} /> Meeting time –– : –– AM ### Hide time zone# When a `ZonedDateTime` object is provided as the value to `useTimeField`, the time zone abbreviation is displayed by default. However, if this is displayed elsewhere or implicit based on the usecase, it can be hidden using the `hideTimeZone` option. <TimeField label="Appointment time" defaultValue={parseZonedDateTime('2022-11-07T10:45[America/Los_Angeles]')} hideTimeZone /> <TimeField label="Appointment time" defaultValue={parseZonedDateTime( '2022-11-07T10:45[America/Los_Angeles]' )} hideTimeZone /> <TimeField label="Appointment time" defaultValue={parseZonedDateTime( '2022-11-07T10:45[America/Los_Angeles]' )} hideTimeZone /> Appointment time 10 : 45 AM ### Hour cycle# By default, `useTimeField` displays times in either 12 or 24 hour hour format depending on the user's locale. However, this can be overridden using the `hourCycle` prop if needed for a specific usecase. This example forces `useTimeField` to use 24-hour time, regardless of the locale. <TimeField label="Appointment time" hourCycle={24} /> <TimeField label="Appointment time" hourCycle={24} /> <TimeField label="Appointment time" hourCycle={24} /> Appointment time –– : –– --- ## Page: https://react-spectrum.adobe.com/react-aria/useCheckbox.html # useCheckbox Provides the behavior and accessibility implementation for a checkbox component. Checkboxes allow users to select multiple items from a list of individual items, or to mark one individual item as selected. <table><tbody><tr><th>install</th><td><code>yarn add react-aria</code></td></tr><tr><th>version</th><td>3.41.1</td></tr><tr><th>usage</th><td><code><span>import</span> {useCheckbox} <span>from</span> <span>'react-aria'</span></code></td></tr></tbody></table> View ARIA pattern W3C View repository GitHub View package NPM ## API# * * * `useCheckbox( props: AriaCheckboxProps , state: ToggleState , inputRef: RefObject <HTMLInputElement | | null> ): CheckboxAria ` ## Features# * * * Checkboxes can be built with the <input> HTML element, but this can be difficult to style. `useCheckbox` helps achieve accessible checkboxes that can be styled as needed. * Built with a native HTML `<input>` element, which can be optionally visually hidden to allow custom styling * Full support for browser features like form autofill * Keyboard focus management and cross browser normalization * Labeling support for assistive technology * Indeterminate state support ## Anatomy# * * * A checkbox consists of a visual selection indicator and a label. Checkboxes support three selection states: checked, unchecked, and indeterminate. Users may click or touch a checkbox to toggle the selection state, or use the Tab key to navigate to it and the Space key to toggle it. `useCheckbox` returns props to be spread onto its input element: | Name | Type | Description | | --- | --- | --- | | `labelProps` | `LabelHTMLAttributes<HTMLLabelElement>` | Props for the label wrapper element. | | `inputProps` | `InputHTMLAttributes<HTMLInputElement>` | Props for the input element. | | `isSelected` | `boolean` | Whether the checkbox is selected. | | `isPressed` | `boolean` | Whether the checkbox is in a pressed state. | | `isDisabled` | `boolean` | Whether the checkbox is disabled. | | `isReadOnly` | `boolean` | Whether the checkbox is read only. | | `isInvalid` | `boolean` | Whether the input value is invalid. | | `validationErrors` | `string[]` | The current error messages for the input if it is invalid, otherwise an empty array. | | `validationDetails` | `ValidityState` | The native validation details for the input. | Selection state is managed by the `useToggleState` hook in `@react-stately/toggle`. The state object should be passed as an option to `useCheckbox`. In most cases, checkboxes should have a visual label. If the checkbox does not have a visible label, an `aria-label` or `aria-labelledby` prop must be passed instead to identify the element to assistive technology. ## Example# * * * import {useToggleState} from 'react-stately'; import {useCheckbox} from 'react-aria'; function Checkbox(props) { let { children } = props; let state = useToggleState(props); let ref = React.useRef(null); let { inputProps } = useCheckbox(props, state, ref); return ( <label style={{ display: 'block' }}> <input {...inputProps} ref={ref} /> {children} </label> ); } <Checkbox>Unsubscribe</Checkbox> import {useToggleState} from 'react-stately'; import {useCheckbox} from 'react-aria'; function Checkbox(props) { let { children } = props; let state = useToggleState(props); let ref = React.useRef(null); let { inputProps } = useCheckbox(props, state, ref); return ( <label style={{ display: 'block' }}> <input {...inputProps} ref={ref} /> {children} </label> ); } <Checkbox>Unsubscribe</Checkbox> import {useToggleState} from 'react-stately'; import {useCheckbox} from 'react-aria'; function Checkbox( props ) { let { children } = props; let state = useToggleState( props ); let ref = React.useRef( null ); let { inputProps } = useCheckbox( props, state, ref ); return ( <label style={{ display: 'block' }} > <input {...inputProps} ref={ref} /> {children} </label> ); } <Checkbox> Unsubscribe </Checkbox> Unsubscribe ## Styling# * * * To build a custom styled checkbox, you can make the native input element visually hidden. This is possible using the < `VisuallyHidden` \> utility component from `@react-aria/visually-hidden`. It is still in the DOM and accessible to assistive technology, but invisible. This example uses SVG to build the visual checkbox, which is hidden from screen readers with `aria-hidden`. For keyboard accessibility, a focus ring is important to indicate which element has keyboard focus. This is implemented with the `useFocusRing` hook from `@react-aria/focus`. When `isFocusVisible` is true, an extra SVG element is rendered to indicate focus. The focus ring is only visible when the user is interacting with a keyboard, not with a mouse or touch. import {mergeProps, useFocusRing, VisuallyHidden} from 'react-aria'; function Checkbox(props) { let state = useToggleState(props); let ref = React.useRef(null); let { inputProps } = useCheckbox(props, state, ref); let { isFocusVisible, focusProps } = useFocusRing(); let isSelected = state.isSelected && !props.isIndeterminate; return ( <label style={{ display: 'flex', alignItems: 'center', opacity: props.isDisabled ? 0.4 : 1 }} > <VisuallyHidden> <input {...mergeProps(inputProps, focusProps)} ref={ref} /> </VisuallyHidden> <svg width={24} height={24} aria-hidden="true" style={{ marginRight: 4 }} > <rect x={isSelected ? 4 : 5} y={isSelected ? 4 : 5} width={isSelected ? 16 : 14} height={isSelected ? 16 : 14} fill={isSelected ? 'orange' : 'none'} stroke={isSelected ? 'none' : 'gray'} strokeWidth={2} /> {isSelected && ( <path transform="translate(7 7)" d={`M3.788 9A.999.999 0 0 1 3 8.615l-2.288-3a1 1 0 1 1 1.576-1.23l1.5 1.991 3.924-4.991a1 1 0 1 1 1.576 1.23l-4.712 6A.999.999 0 0 1 3.788 9z`} /> )} {props.isIndeterminate && <rect x={7} y={11} width={10} height={2} fill="gray" />} {isFocusVisible && ( <rect x={1} y={1} width={22} height={22} fill="none" stroke="orange" strokeWidth={2} /> )} </svg> {props.children} </label> ); } <Checkbox>Unsubscribe</Checkbox> import { mergeProps, useFocusRing, VisuallyHidden } from 'react-aria'; function Checkbox(props) { let state = useToggleState(props); let ref = React.useRef(null); let { inputProps } = useCheckbox(props, state, ref); let { isFocusVisible, focusProps } = useFocusRing(); let isSelected = state.isSelected && !props.isIndeterminate; return ( <label style={{ display: 'flex', alignItems: 'center', opacity: props.isDisabled ? 0.4 : 1 }} > <VisuallyHidden> <input {...mergeProps(inputProps, focusProps)} ref={ref} /> </VisuallyHidden> <svg width={24} height={24} aria-hidden="true" style={{ marginRight: 4 }} > <rect x={isSelected ? 4 : 5} y={isSelected ? 4 : 5} width={isSelected ? 16 : 14} height={isSelected ? 16 : 14} fill={isSelected ? 'orange' : 'none'} stroke={isSelected ? 'none' : 'gray'} strokeWidth={2} /> {isSelected && ( <path transform="translate(7 7)" d={`M3.788 9A.999.999 0 0 1 3 8.615l-2.288-3a1 1 0 1 1 1.576-1.23l1.5 1.991 3.924-4.991a1 1 0 1 1 1.576 1.23l-4.712 6A.999.999 0 0 1 3.788 9z`} /> )} {props.isIndeterminate && ( <rect x={7} y={11} width={10} height={2} fill="gray" /> )} {isFocusVisible && ( <rect x={1} y={1} width={22} height={22} fill="none" stroke="orange" strokeWidth={2} /> )} </svg> {props.children} </label> ); } <Checkbox>Unsubscribe</Checkbox> import { mergeProps, useFocusRing, VisuallyHidden } from 'react-aria'; function Checkbox( props ) { let state = useToggleState( props ); let ref = React.useRef( null ); let { inputProps } = useCheckbox( props, state, ref ); let { isFocusVisible, focusProps } = useFocusRing(); let isSelected = state.isSelected && !props .isIndeterminate; return ( <label style={{ display: 'flex', alignItems: 'center', opacity: props .isDisabled ? 0.4 : 1 }} > <VisuallyHidden> <input {...mergeProps( inputProps, focusProps )} ref={ref} /> </VisuallyHidden> <svg width={24} height={24} aria-hidden="true" style={{ marginRight: 4 }} > <rect x={isSelected ? 4 : 5} y={isSelected ? 4 : 5} width={isSelected ? 16 : 14} height={isSelected ? 16 : 14} fill={isSelected ? 'orange' : 'none'} stroke={isSelected ? 'none' : 'gray'} strokeWidth={2} /> {isSelected && ( <path transform="translate(7 7)" d={`M3.788 9A.999.999 0 0 1 3 8.615l-2.288-3a1 1 0 1 1 1.576-1.23l1.5 1.991 3.924-4.991a1 1 0 1 1 1.576 1.23l-4.712 6A.999.999 0 0 1 3.788 9z`} /> )} {props .isIndeterminate && ( <rect x={7} y={11} width={10} height={2} fill="gray" /> )} {isFocusVisible && ( <rect x={1} y={1} width={22} height={22} fill="none" stroke="orange" strokeWidth={2} /> )} </svg> {props.children} </label> ); } <Checkbox> Unsubscribe </Checkbox> Unsubscribe ## Styled examples# * * * Tailwind CSS An animated Checkbox built with Tailwind and React Aria. ## Usage# * * * The following examples show how to use the `Checkbox` component created in the above example. ### Default value# Checkboxes are not selected by default. The `defaultSelected` prop can be used to set the default state. <Checkbox defaultSelected>Subscribe</Checkbox> <Checkbox defaultSelected>Subscribe</Checkbox> <Checkbox defaultSelected > Subscribe </Checkbox> Subscribe ### Controlled value# The `isSelected` prop can be used to make the selected state controlled. The `onChange` event is fired when the user presses the checkbox, and receives the new value. function Example() { let [selected, setSelection] = React.useState(false); return ( <> <Checkbox isSelected={selected} onChange={setSelection}> Subscribe </Checkbox> <p>{`You are ${selected ? 'subscribed' : 'unsubscribed'}`}</p> </> ); } function Example() { let [selected, setSelection] = React.useState(false); return ( <> <Checkbox isSelected={selected} onChange={setSelection} > Subscribe </Checkbox> <p> {`You are ${ selected ? 'subscribed' : 'unsubscribed' }`} </p> </> ); } function Example() { let [ selected, setSelection ] = React.useState( false ); return ( <> <Checkbox isSelected={selected} onChange={setSelection} > Subscribe </Checkbox> <p> {`You are ${ selected ? 'subscribed' : 'unsubscribed' }`} </p> </> ); } Subscribe You are unsubscribed ### Indeterminate# A Checkbox can be in an indeterminate state, controlled using the `isIndeterminate` prop. This overrides the appearance of the Checkbox, whether selection is controlled or uncontrolled. The Checkbox will visually remain indeterminate until the `isIndeterminate` prop is set to false, regardless of user interaction. <Checkbox isIndeterminate>Subscribe</Checkbox> <Checkbox isIndeterminate>Subscribe</Checkbox> <Checkbox isIndeterminate > Subscribe </Checkbox> Subscribe ### Disabled# Checkboxes can be disabled using the `isDisabled` prop. <Checkbox isDisabled>Subscribe</Checkbox> <Checkbox isDisabled>Subscribe</Checkbox> <Checkbox isDisabled> Subscribe </Checkbox> Subscribe ### Read only# The `isReadOnly` prop makes the selection immutable. Unlike `isDisabled`, the Checkbox remains focusable. See the MDN docs for more information. <Checkbox isSelected isReadOnly>Agree</Checkbox> <Checkbox isSelected isReadOnly>Agree</Checkbox> <Checkbox isSelected isReadOnly > Agree </Checkbox> Agree ### HTML forms# Checkbox supports the `name` and `value` props for integration with HTML forms. <Checkbox name="newsletter" value="subscribe">Subscribe</Checkbox> <Checkbox name="newsletter" value="subscribe"> Subscribe </Checkbox> <Checkbox name="newsletter" value="subscribe" > Subscribe </Checkbox> Subscribe ## Internationalization# * * * ### RTL# In right-to-left languages, the checkbox should be mirrored. The checkbox should be placed on the right side of the label. Ensure that your CSS accounts for this. --- ## Page: https://react-spectrum.adobe.com/react-aria/useCheckboxGroup.html # useCheckboxGroup Provides the behavior and accessibility implementation for a checkbox group component. Checkbox groups allow users to select multiple items from a list of options. <table><tbody><tr><th>install</th><td><code>yarn add react-aria</code></td></tr><tr><th>version</th><td>3.41.1</td></tr><tr><th>usage</th><td><code><span>import</span> {useCheckboxGroup, useCheckboxGroupItem} <span>from</span> <span>'react-aria'</span></code></td></tr></tbody></table> View ARIA pattern W3C View repository GitHub View package NPM ## API# * * * `useCheckboxGroup( (props: AriaCheckboxGroupProps , , state: CheckboxGroupState )): CheckboxGroupAria ``useCheckboxGroupItem( props: AriaCheckboxGroupItemProps , state: CheckboxGroupState , inputRef: RefObject <HTMLInputElement | | null> ): CheckboxAria ` ## Features# * * * Checkbox groups can be built in HTML with the <fieldset> and <input> elements, however these can be difficult to style. `useCheckboxGroup` and `useCheckboxGroupItem` help achieve accessible checkbox groups that can be styled as needed. * Checkbox groups are exposed to assistive technology via ARIA * Each checkbox is built with a native HTML `<input>` element, which can be optionally visually hidden to allow custom styling * Full support for browser features like form autofill and validation * Keyboard focus management and cross browser normalization * Group and checkbox labeling support for assistive technology ## Anatomy# * * * A checkbox group consists of a set of checkboxes, and a label. Each checkbox includes a label and a visual selection indicator. Zero or more checkboxes within the group can be selected at a time. Users may click or touch a checkbox to select it, or use the Tab key to navigate to it and the Space key to toggle it. `useCheckboxGroup` returns props for the group and its label, which you should spread onto the appropriate element: | Name | Type | Description | | --- | --- | --- | | `groupProps` | `DOMAttributes` | Props for the checkbox group wrapper element. | | `labelProps` | `DOMAttributes` | Props for the checkbox group's visible label (if any). | | `descriptionProps` | `DOMAttributes` | Props for the checkbox group description element, if any. | | `errorMessageProps` | `DOMAttributes` | Props for the checkbox group error message element, if any. | | `isInvalid` | `boolean` | Whether the input value is invalid. | | `validationErrors` | `string[]` | The current error messages for the input if it is invalid, otherwise an empty array. | | `validationDetails` | `ValidityState` | The native validation details for the input. | `useCheckboxGroupItem` returns props for an individual checkbox: | Name | Type | Description | | --- | --- | --- | | `labelProps` | `LabelHTMLAttributes<HTMLLabelElement>` | Props for the label wrapper element. | | `inputProps` | `InputHTMLAttributes<HTMLInputElement>` | Props for the input element. | | `isSelected` | `boolean` | Whether the checkbox is selected. | | `isPressed` | `boolean` | Whether the checkbox is in a pressed state. | | `isDisabled` | `boolean` | Whether the checkbox is disabled. | | `isReadOnly` | `boolean` | Whether the checkbox is read only. | | `isInvalid` | `boolean` | Whether the input value is invalid. | | `validationErrors` | `string[]` | The current error messages for the input if it is invalid, otherwise an empty array. | | `validationDetails` | `ValidityState` | The native validation details for the input. | Selection state is managed by the `useCheckboxGroupState` hook in `@react-stately/checkbox`. The state object should be passed as an option to `useCheckboxGroup` and `useCheckboxGroupItem`. Individual checkboxes must have a visual label. If the checkbox group does not have a visible label, an `aria-label` or `aria-labelledby` prop must be passed instead to identify the element to assistive technology. **Note:** `useCheckboxGroupItem` should only be used when it is contained within a checkbox group. For a standalone checkbox, use the useCheckbox hook instead. ## Example# * * * This example uses native input elements for the checkboxes, and React context to share state from the group to each checkbox. An HTML `<label>` element wraps the native input and the text to provide an implicit label for the checkbox. import {useCheckboxGroupState} from 'react-stately'; import {useCheckboxGroup, useCheckboxGroupItem} from 'react-aria'; let CheckboxGroupContext = React.createContext(null); function CheckboxGroup(props) { let { children, label, description } = props; let state = useCheckboxGroupState(props); let { groupProps, labelProps, descriptionProps, errorMessageProps, isInvalid, validationErrors } = useCheckboxGroup(props, state); return ( <div {...groupProps}> <span {...labelProps}>{label}</span> <CheckboxGroupContext.Provider value={state}> {children} </CheckboxGroupContext.Provider> {description && ( <div {...descriptionProps} style={{ fontSize: 12 }}>{description}</div> )} {isInvalid && ( <div {...errorMessageProps} style={{ color: 'red', fontSize: 12 }}> {validationErrors.join(' ')} </div> )} </div> ); } function Checkbox(props) { let { children } = props; let state = React.useContext(CheckboxGroupContext); let ref = React.useRef(null); let { inputProps } = useCheckboxGroupItem(props, state, ref); let isDisabled = state.isDisabled || props.isDisabled; let isSelected = state.isSelected(props.value); return ( <label style={{ display: 'block', color: (isDisabled && 'var(--gray)') || (isSelected && 'var(--blue)') }} > <input {...inputProps} ref={ref} /> {children} </label> ); } <CheckboxGroup label="Favorite sports"> <Checkbox value="soccer" isDisabled>Soccer</Checkbox> <Checkbox value="baseball">Baseball</Checkbox> <Checkbox value="basketball">Basketball</Checkbox> </CheckboxGroup> import {useCheckboxGroupState} from 'react-stately'; import { useCheckboxGroup, useCheckboxGroupItem } from 'react-aria'; let CheckboxGroupContext = React.createContext(null); function CheckboxGroup(props) { let { children, label, description } = props; let state = useCheckboxGroupState(props); let { groupProps, labelProps, descriptionProps, errorMessageProps, isInvalid, validationErrors } = useCheckboxGroup(props, state); return ( <div {...groupProps}> <span {...labelProps}>{label}</span> <CheckboxGroupContext.Provider value={state}> {children} </CheckboxGroupContext.Provider> {description && ( <div {...descriptionProps} style={{ fontSize: 12 }}> {description} </div> )} {isInvalid && ( <div {...errorMessageProps} style={{ color: 'red', fontSize: 12 }} > {validationErrors.join(' ')} </div> )} </div> ); } function Checkbox(props) { let { children } = props; let state = React.useContext(CheckboxGroupContext); let ref = React.useRef(null); let { inputProps } = useCheckboxGroupItem( props, state, ref ); let isDisabled = state.isDisabled || props.isDisabled; let isSelected = state.isSelected(props.value); return ( <label style={{ display: 'block', color: (isDisabled && 'var(--gray)') || (isSelected && 'var(--blue)') }} > <input {...inputProps} ref={ref} /> {children} </label> ); } <CheckboxGroup label="Favorite sports"> <Checkbox value="soccer" isDisabled>Soccer</Checkbox> <Checkbox value="baseball">Baseball</Checkbox> <Checkbox value="basketball">Basketball</Checkbox> </CheckboxGroup> import {useCheckboxGroupState} from 'react-stately'; import { useCheckboxGroup, useCheckboxGroupItem } from 'react-aria'; let CheckboxGroupContext = React.createContext( null ); function CheckboxGroup( props ) { let { children, label, description } = props; let state = useCheckboxGroupState( props ); let { groupProps, labelProps, descriptionProps, errorMessageProps, isInvalid, validationErrors } = useCheckboxGroup( props, state ); return ( <div {...groupProps}> <span {...labelProps} > {label} </span> <CheckboxGroupContext.Provider value={state} > {children} </CheckboxGroupContext.Provider> {description && ( <div {...descriptionProps} style={{ fontSize: 12 }} > {description} </div> )} {isInvalid && ( <div {...errorMessageProps} style={{ color: 'red', fontSize: 12 }} > {validationErrors .join(' ')} </div> )} </div> ); } function Checkbox( props ) { let { children } = props; let state = React .useContext( CheckboxGroupContext ); let ref = React.useRef( null ); let { inputProps } = useCheckboxGroupItem( props, state, ref ); let isDisabled = state.isDisabled || props.isDisabled; let isSelected = state .isSelected( props.value ); return ( <label style={{ display: 'block', color: (isDisabled && 'var(--gray)') || (isSelected && 'var(--blue)') }} > <input {...inputProps} ref={ref} /> {children} </label> ); } <CheckboxGroup label="Favorite sports"> <Checkbox value="soccer" isDisabled > Soccer </Checkbox> <Checkbox value="baseball"> Baseball </Checkbox> <Checkbox value="basketball"> Basketball </Checkbox> </CheckboxGroup> Favorite sportsSoccerBaseballBasketball ## Styling# * * * See the useCheckbox docs for details on how to customize the styling of checkbox elements. ## Styled examples# * * * Button Group A multi-selectable segmented ButtonGroup component. ## Usage# * * * The following examples show how to use the `CheckboxGroup` component created in the above example. ### Default value# An initial, uncontrolled value can be provided to the CheckboxGroup using the `defaultValue` prop, which accepts an array of selected items that map to the `value` prop on each Checkbox. <CheckboxGroup label="Favorite sports (uncontrolled)" defaultValue={['soccer', 'baseball']} > <Checkbox value="soccer">Soccer</Checkbox> <Checkbox value="baseball">Baseball</Checkbox> <Checkbox value="basketball">Basketball</Checkbox> </CheckboxGroup> <CheckboxGroup label="Favorite sports (uncontrolled)" defaultValue={['soccer', 'baseball']} > <Checkbox value="soccer">Soccer</Checkbox> <Checkbox value="baseball">Baseball</Checkbox> <Checkbox value="basketball">Basketball</Checkbox> </CheckboxGroup> <CheckboxGroup label="Favorite sports (uncontrolled)" defaultValue={[ 'soccer', 'baseball' ]} > <Checkbox value="soccer"> Soccer </Checkbox> <Checkbox value="baseball"> Baseball </Checkbox> <Checkbox value="basketball"> Basketball </Checkbox> </CheckboxGroup> Favorite sports (uncontrolled)SoccerBaseballBasketball ### Controlled value# A controlled value can be provided using the `value` prop, which accepts an array of selected items, which map to the `value` prop on each Checkbox. The `onChange` event is fired when the user checks or unchecks an option. It receives a new array containing the updated selected values. function Example() { let [selected, setSelected] = React.useState(['soccer', 'baseball']); return ( <CheckboxGroup label="Favorite sports (controlled)" value={selected} onChange={setSelected} > <Checkbox value="soccer">Soccer</Checkbox> <Checkbox value="baseball">Baseball</Checkbox> <Checkbox value="basketball">Basketball</Checkbox> </CheckboxGroup> ); } function Example() { let [selected, setSelected] = React.useState([ 'soccer', 'baseball' ]); return ( <CheckboxGroup label="Favorite sports (controlled)" value={selected} onChange={setSelected} > <Checkbox value="soccer">Soccer</Checkbox> <Checkbox value="baseball">Baseball</Checkbox> <Checkbox value="basketball">Basketball</Checkbox> </CheckboxGroup> ); } function Example() { let [ selected, setSelected ] = React.useState([ 'soccer', 'baseball' ]); return ( <CheckboxGroup label="Favorite sports (controlled)" value={selected} onChange={setSelected} > <Checkbox value="soccer"> Soccer </Checkbox> <Checkbox value="baseball"> Baseball </Checkbox> <Checkbox value="basketball"> Basketball </Checkbox> </CheckboxGroup> ); } Favorite sports (controlled)SoccerBaseballBasketball ### Description# The `description` prop can be used to associate additional help text with a checkbox group. <CheckboxGroup label="Favorite sports" description="Select your favorite sports." > <Checkbox value="soccer">Soccer</Checkbox> <Checkbox value="baseball">Baseball</Checkbox> <Checkbox value="basketball">Basketball</Checkbox> </CheckboxGroup> <CheckboxGroup label="Favorite sports" description="Select your favorite sports." > <Checkbox value="soccer">Soccer</Checkbox> <Checkbox value="baseball">Baseball</Checkbox> <Checkbox value="basketball">Basketball</Checkbox> </CheckboxGroup> <CheckboxGroup label="Favorite sports" description="Select your favorite sports." > <Checkbox value="soccer"> Soccer </Checkbox> <Checkbox value="baseball"> Baseball </Checkbox> <Checkbox value="basketball"> Basketball </Checkbox> </CheckboxGroup> Favorite sportsSoccerBaseballBasketball Select your favorite sports. ### Group validation# CheckboxGroup supports the `isRequired` prop to ensure the user selects at least one item, as well as custom client and server-side validation. Individual checkboxes also support validation, and errors from all checkboxes are aggregated at the group level. CheckboxGroup can also be integrated with other form libraries. See the Forms guide to learn more. When a CheckboxGroup has the `validationBehavior="native"` prop, validation errors block form submission. The `isRequired` prop at the `CheckboxGroup` level requires that at least one item is selected. To display validation errors, use the `validationErrors` and `errorMessageProps` returned by `useCheckboxGroup`. This allows you to render error messages from all of the above sources with consistent custom styles. <form> <CheckboxGroup label="Sandwich condiments" name="condiments" isRequired validationBehavior="native" > <Checkbox value="lettuce">Lettuce</Checkbox> <Checkbox value="tomato">Tomato</Checkbox> <Checkbox value="onion">Onion</Checkbox> <Checkbox value="sprouts">Sprouts</Checkbox> </CheckboxGroup> <input type="submit" style={{marginTop: 8}} /> </form> <form> <CheckboxGroup label="Sandwich condiments" name="condiments" isRequired validationBehavior="native" > <Checkbox value="lettuce">Lettuce</Checkbox> <Checkbox value="tomato">Tomato</Checkbox> <Checkbox value="onion">Onion</Checkbox> <Checkbox value="sprouts">Sprouts</Checkbox> </CheckboxGroup> <input type="submit" style={{marginTop: 8}} /> </form> <form> <CheckboxGroup label="Sandwich condiments" name="condiments" isRequired validationBehavior="native" > <Checkbox value="lettuce"> Lettuce </Checkbox> <Checkbox value="tomato"> Tomato </Checkbox> <Checkbox value="onion"> Onion </Checkbox> <Checkbox value="sprouts"> Sprouts </Checkbox> </CheckboxGroup> <input type="submit" style={{ marginTop: 8 }} /> </form> Sandwich condimentsLettuceTomatoOnionSprouts ### Individual Checkbox validation# To require that specific checkboxes are checked, set the `isRequired` prop at the `Checkbox` level instead of the `CheckboxGroup`. The following example shows how to require that all items are selected. <form> <CheckboxGroup label="Agree to the following" validationBehavior="native"> <Checkbox value="terms" isRequired>Terms and conditions</Checkbox> <Checkbox value="privacy" isRequired>Privacy policy</Checkbox> <Checkbox value="cookies" isRequired>Cookie policy</Checkbox> </CheckboxGroup> <input type="submit" style={{marginTop: 8}} /> </form> <form> <CheckboxGroup label="Agree to the following" validationBehavior="native" > <Checkbox value="terms" isRequired> Terms and conditions </Checkbox> <Checkbox value="privacy" isRequired> Privacy policy </Checkbox> <Checkbox value="cookies" isRequired> Cookie policy </Checkbox> </CheckboxGroup> <input type="submit" style={{ marginTop: 8 }} /> </form> <form> <CheckboxGroup label="Agree to the following" validationBehavior="native" > <Checkbox value="terms" isRequired > Terms and conditions </Checkbox> <Checkbox value="privacy" isRequired > Privacy policy </Checkbox> <Checkbox value="cookies" isRequired > Cookie policy </Checkbox> </CheckboxGroup> <input type="submit" style={{ marginTop: 8 }} /> </form> Agree to the followingTerms and conditionsPrivacy policyCookie policy ### Disabled# The entire CheckboxGroup can be disabled with the `isDisabled` prop. <CheckboxGroup label="Favorite sports" isDisabled> <Checkbox value="soccer">Soccer</Checkbox> <Checkbox value="baseball">Baseball</Checkbox> <Checkbox value="basketball">Basketball</Checkbox> </CheckboxGroup> <CheckboxGroup label="Favorite sports" isDisabled> <Checkbox value="soccer">Soccer</Checkbox> <Checkbox value="baseball">Baseball</Checkbox> <Checkbox value="basketball">Basketball</Checkbox> </CheckboxGroup> <CheckboxGroup label="Favorite sports" isDisabled > <Checkbox value="soccer"> Soccer </Checkbox> <Checkbox value="baseball"> Baseball </Checkbox> <Checkbox value="basketball"> Basketball </Checkbox> </CheckboxGroup> Favorite sportsSoccerBaseballBasketball To disable an individual checkbox, pass `isDisabled` to the `Checkbox` instead. <CheckboxGroup label="Favorite sports"> <Checkbox value="soccer">Soccer</Checkbox> <Checkbox value="baseball" isDisabled>Baseball</Checkbox> <Checkbox value="basketball">Basketball</Checkbox> </CheckboxGroup> <CheckboxGroup label="Favorite sports"> <Checkbox value="soccer">Soccer</Checkbox> <Checkbox value="baseball" isDisabled>Baseball</Checkbox> <Checkbox value="basketball">Basketball</Checkbox> </CheckboxGroup> <CheckboxGroup label="Favorite sports"> <Checkbox value="soccer"> Soccer </Checkbox> <Checkbox value="baseball" isDisabled > Baseball </Checkbox> <Checkbox value="basketball"> Basketball </Checkbox> </CheckboxGroup> Favorite sportsSoccerBaseballBasketball ### Read only# The `isReadOnly` prop makes the selection immutable. Unlike `isDisabled`, the CheckboxGroup remains focusable. See the MDN docs for more information. <CheckboxGroup label="Favorite sports" defaultValue={['baseball']} isReadOnly> <Checkbox value="soccer">Soccer</Checkbox> <Checkbox value="baseball">Baseball</Checkbox> <Checkbox value="basketball">Basketball</Checkbox> </CheckboxGroup> <CheckboxGroup label="Favorite sports" defaultValue={['baseball']} isReadOnly > <Checkbox value="soccer">Soccer</Checkbox> <Checkbox value="baseball">Baseball</Checkbox> <Checkbox value="basketball">Basketball</Checkbox> </CheckboxGroup> <CheckboxGroup label="Favorite sports" defaultValue={[ 'baseball' ]} isReadOnly > <Checkbox value="soccer"> Soccer </Checkbox> <Checkbox value="baseball"> Baseball </Checkbox> <Checkbox value="basketball"> Basketball </Checkbox> </CheckboxGroup> Favorite sportsSoccerBaseballBasketball ### HTML forms# CheckboxGroup supports the `name` prop, paired with the Checkbox `value` prop, for integration with HTML forms. <CheckboxGroup label="Favorite sports" name="sports"> <Checkbox value="soccer">Soccer</Checkbox> <Checkbox value="baseball">Baseball</Checkbox> <Checkbox value="basketball">Basketball</Checkbox> </CheckboxGroup> <CheckboxGroup label="Favorite sports" name="sports"> <Checkbox value="soccer">Soccer</Checkbox> <Checkbox value="baseball">Baseball</Checkbox> <Checkbox value="basketball">Basketball</Checkbox> </CheckboxGroup> <CheckboxGroup label="Favorite sports" name="sports" > <Checkbox value="soccer"> Soccer </Checkbox> <Checkbox value="baseball"> Baseball </Checkbox> <Checkbox value="basketball"> Basketball </Checkbox> </CheckboxGroup> Favorite sportsSoccerBaseballBasketball --- ## Page: https://react-spectrum.adobe.com/react-aria/useNumberField.html # useNumberField Provides the behavior and accessibility implementation for a number field component. Number fields allow users to enter a number, and increment or decrement the value using stepper buttons. <table><tbody><tr><th>install</th><td><code>yarn add react-aria</code></td></tr><tr><th>version</th><td>3.41.1</td></tr><tr><th>usage</th><td><code><span>import</span> {useNumberField} <span>from</span> <span>'react-aria'</span></code></td></tr></tbody></table> View ARIA pattern W3C View repository GitHub View package NPM ## API# * * * `useNumberField( props: AriaNumberFieldProps , state: NumberFieldState , inputRef: RefObject<HTMLInputElement | | null> ): NumberFieldAria ` ## Features# * * * Number fields can be built with `<input type="number">`, but the behavior is very inconsistent across browsers and platforms, it supports limited localized formatting options, and it is challenging to style the stepper buttons. `useNumberField` helps achieve accessible number fields that support internationalized formatting options and can be styled as needed. * Support for internationalized number formatting and parsing including decimals, percentages, currency values, and units * Support for the Latin, Arabic, and Han positional decimal numbering systems in over 30 locales * Automatically detects the numbering system used and supports parsing numbers not in the default numbering system for the locale * Support for multiple currency formats including symbol, code, and name in standard or accounting notation * Validates keyboard entry as the user types so that only valid numeric input according to the locale and numbering system is accepted * Handles composed input from input method editors, e.g. Pinyin * Automatically selects an appropriate software keyboard for mobile according to the current platform and allowed values * Supports rounding to a configurable number of fraction digits * Support for clamping the value between a configurable minimum and maximum, and snapping to a step value * Support for stepper buttons and arrow keys to increment and decrement the value according to the step value * Supports pressing and holding the stepper buttons to continuously increment or decrement * Handles floating point rounding errors when incrementing, decrementing, and snapping to step * Supports using the scroll wheel to increment and decrement the value * Exposed to assistive technology as a text field with a custom localized role description using ARIA * Follows the spinbutton ARIA pattern * Works around bugs in VoiceOver with the spinbutton role * Uses an ARIA live region to ensure that value changes are announced * Support for native HTML constraint validation with customizable UI, custom validation functions, realtime validation, and server-side validation errors Read our blog post for more details about the interactions and internationalization support implemented by `useNumberField`. ## Anatomy# * * * Number fields consist of an input element that shows the current value and allows the user to type a new value, optional stepper buttons to increment and decrement the value, a group containing the input and stepper buttons, and a label. `useNumberField` also supports optional description and error message elements, which can be used to provide more context about the field, and any validation messages. These are linked with the input via the `aria-describedby` attribute. `useNumberField` returns props for each of these, which you should spread onto the appropriate elements: | Name | Type | Description | | --- | --- | --- | | `labelProps` | `LabelHTMLAttributes<HTMLLabelElement>` | Props for the label element. | | `groupProps` | ` GroupDOMAttributes ` | Props for the group wrapper around the input and stepper buttons. | | `inputProps` | `InputHTMLAttributes<HTMLInputElement>` | Props for the input element. | | `incrementButtonProps` | ` AriaButtonProps ` | Props for the increment button, to be passed to useButton. | | `decrementButtonProps` | ` AriaButtonProps ` | Props for the decrement button, to be passed to useButton. | | `descriptionProps` | `DOMAttributes` | Props for the number field's description element, if any. | | `errorMessageProps` | `DOMAttributes` | Props for the number field's error message element, if any. | | `isInvalid` | `boolean` | Whether the input value is invalid. | | `validationErrors` | `string[]` | The current error messages for the input if it is invalid, otherwise an empty array. | | `validationDetails` | `ValidityState` | The native validation details for the input. | State is managed by the `useNumberFieldState` hook from `@react-stately/numberfield`. The state object should be passed as an option to `useNumberField` If there is no visual label, an `aria-label` or `aria-labelledby` prop must be passed instead to identify the element to screen readers. ## Example# * * * The following example shows how to build a simple number field. It includes an input element where the user can type a number, along with increment and decrement buttons on either side. The `Button` component used in this example is independent and can be used separately from `NumberField`. **Note:** Due to a bug in Safari on macOS, pointer events may not be dispatched after a `<button>` element is disabled while the mouse is pressed. This may require the user to click twice when incrementing or decrementing the value from the minimum or maximum value. While out of scope for this example, you may wish to use a `<div>` element instead of a `<button>` to avoid this issue. See the useButton docs for an example of a button with a custom element type. In addition, see useTextField for an example of the description and error message elements. import {useNumberFieldState} from 'react-stately'; import {useLocale, useNumberField} from 'react-aria'; // Reuse the Button from your component library. See below for details. import {Button} from 'your-component-library'; function NumberField(props) { let { locale } = useLocale(); let state = useNumberFieldState({ ...props, locale }); let inputRef = React.useRef(null); let { labelProps, groupProps, inputProps, incrementButtonProps, decrementButtonProps } = useNumberField(props, state, inputRef); return ( <div> <label {...labelProps}>{props.label}</label> <div {...groupProps}> <Button {...decrementButtonProps}>-</Button> <input {...inputProps} ref={inputRef} /> <Button {...incrementButtonProps}>+</Button> </div> </div> ); } <NumberField label="Price" defaultValue={6} formatOptions={{ style: 'currency', currency: 'USD' }} /> import {useNumberFieldState} from 'react-stately'; import {useLocale, useNumberField} from 'react-aria'; // Reuse the Button from your component library. See below for details. import {Button} from 'your-component-library'; function NumberField(props) { let { locale } = useLocale(); let state = useNumberFieldState({ ...props, locale }); let inputRef = React.useRef(null); let { labelProps, groupProps, inputProps, incrementButtonProps, decrementButtonProps } = useNumberField(props, state, inputRef); return ( <div> <label {...labelProps}>{props.label}</label> <div {...groupProps}> <Button {...decrementButtonProps}>-</Button> <input {...inputProps} ref={inputRef} /> <Button {...incrementButtonProps}>+</Button> </div> </div> ); } <NumberField label="Price" defaultValue={6} formatOptions={{ style: 'currency', currency: 'USD' }} /> import {useNumberFieldState} from 'react-stately'; import { useLocale, useNumberField } from 'react-aria'; // Reuse the Button from your component library. See below for details. import {Button} from 'your-component-library'; function NumberField( props ) { let { locale } = useLocale(); let state = useNumberFieldState({ ...props, locale }); let inputRef = React .useRef(null); let { labelProps, groupProps, inputProps, incrementButtonProps, decrementButtonProps } = useNumberField( props, state, inputRef ); return ( <div> <label {...labelProps} > {props.label} </label> <div {...groupProps} > <Button {...decrementButtonProps} > - </Button> <input {...inputProps} ref={inputRef} /> <Button {...incrementButtonProps} > + </Button> </div> </div> ); } <NumberField label="Price" defaultValue={6} formatOptions={{ style: 'currency', currency: 'USD' }} /> Price \-+ ### Button# The `Button` component is used in the above example to increment and decrement the value. It is built using the useButton hook, and can be shared with many other components. Show code import {useButton} from 'react-aria'; function Button(props) { let ref = React.useRef(null); let { buttonProps } = useButton(props, ref); return <button {...buttonProps} ref={ref}>{props.children}</button>; } import {useButton} from 'react-aria'; function Button(props) { let ref = React.useRef(null); let { buttonProps } = useButton(props, ref); return ( <button {...buttonProps} ref={ref}> {props.children} </button> ); } import {useButton} from 'react-aria'; function Button(props) { let ref = React.useRef( null ); let { buttonProps } = useButton( props, ref ); return ( <button {...buttonProps} ref={ref} > {props.children} </button> ); } ## Usage# * * * The following examples show how to use the `NumberField` component created in the above example. ### Controlled# By default, `NumberField` is uncontrolled. However, when using the `value` prop, it becomes controlled. This allows you to store the current value in your own state, and use it elsewhere. The `onChange` event is triggered whenever the number value updates. This happens when the user types a value and blurs the input, or when incrementing or decrementing the value. It does not happen as the user types because partial input may not be parseable to a valid number. function Example() { let [value, setValue] = React.useState(6); return ( <> <NumberField label="Controlled value" value={value} onChange={setValue} /> <div>Current value prop: {value}</div> </> ); } function Example() { let [value, setValue] = React.useState(6); return ( <> <NumberField label="Controlled value" value={value} onChange={setValue} /> <div>Current value prop: {value}</div> </> ); } function Example() { let [value, setValue] = React.useState(6); return ( <> <NumberField label="Controlled value" value={value} onChange={setValue} /> <div> Current value prop: {value} </div> </> ); } Controlled value \-+ Current value prop: 6 ### Decimals# The default formatting style for `NumberField` is decimal, but you can configure various aspects via the `formatOptions` prop. All options supported by Intl.NumberFormat are supported, including configuration of minimum and maximum fraction digits, sign display, grouping separators, etc. Currently only standard notation is supported, though scientific, engineering, and compact notation may be supported in the future. The following example uses the `signDisplay` option to include the plus sign for positive numbers, for example to display an offset from some value. In addition, it always displays a minimum of 1 digit after the decimal point, and allows up to 2 fraction digits. If the user enters more than 2 fraction digits, the result will be rounded. <NumberField label="Adjust exposure" defaultValue={0} formatOptions={{ signDisplay: 'exceptZero', minimumFractionDigits: 1, maximumFractionDigits: 2 }} /> <NumberField label="Adjust exposure" defaultValue={0} formatOptions={{ signDisplay: 'exceptZero', minimumFractionDigits: 1, maximumFractionDigits: 2 }} /> <NumberField label="Adjust exposure" defaultValue={0} formatOptions={{ signDisplay: 'exceptZero', minimumFractionDigits: 1, maximumFractionDigits: 2 }} /> Adjust exposure \-+ ### Percentages# The `style: 'percent'` option can be passed to the `formatOptions` prop to treat the value as a percentage. In this mode, the value is multiplied by 100 before it is displayed, i.e. `0.45` is displayed as `45%`. The reverse is also true: when the user enters a value, the `onChange` event will be triggered with the entered value divided by 100. When the percent option is enabled, the default step automatically changes to `0.01` such that incrementing and decrementing occurs by `1%`. This can be overridden with the `step` prop. See below for details. <NumberField label="Sales tax" defaultValue={0.05} formatOptions={{ style: 'percent' }} /> <NumberField label="Sales tax" defaultValue={0.05} formatOptions={{ style: 'percent' }} /> <NumberField label="Sales tax" defaultValue={0.05} formatOptions={{ style: 'percent' }} /> Sales tax \-+ ### Currency values# The `style: 'currency'` option can be passed to the `formatOptions` prop to treat the value as a currency value. The `currency` option must also be passed to set the currency code (e.g. `USD`) to use. In addition, the `currencyDisplay` option can be used to choose whether to display the currency symbol, currency code, or currency name. Finally, the `currencySign` option can be set to `accounting` to use accounting notation for negative numbers, which uses parentheses rather than a minus sign in some locales. If you need to allow the user to change the currency, you should include a separate dropdown next to the number field. The number field itself will not determine the currency from the user input. <NumberField label="Transaction amount" defaultValue={45} formatOptions={{ style: 'currency', currency: 'EUR', currencyDisplay: 'code', currencySign: 'accounting' }} /> <NumberField label="Transaction amount" defaultValue={45} formatOptions={{ style: 'currency', currency: 'EUR', currencyDisplay: 'code', currencySign: 'accounting' }} /> <NumberField label="Transaction amount" defaultValue={45} formatOptions={{ style: 'currency', currency: 'EUR', currencyDisplay: 'code', currencySign: 'accounting' }} /> Transaction amount \-+ ### Units# The `style: 'unit'` option can be passed to the `formatOptions` prop to format the value with a unit of measurement. The `unit` option must also be passed to set which unit to use (e.g. `inch`). In addition, the `unitDisplay` option can be used to choose whether to display the unit in long, short, or narrow format. If you need to allow the user to change the unit, you should include a separate dropdown next to the number field. The number field itself will not determine the unit from the user input. **Note:** the unit style is not currently supported in Safari. A polyfill may be necessary. <NumberField label="Package width" defaultValue={4} formatOptions={{ style: 'unit', unit: 'inch', unitDisplay: 'long' }} /> <NumberField label="Package width" defaultValue={4} formatOptions={{ style: 'unit', unit: 'inch', unitDisplay: 'long' }} /> <NumberField label="Package width" defaultValue={4} formatOptions={{ style: 'unit', unit: 'inch', unitDisplay: 'long' }} /> Package width \-+ ### Minimum and maximum values# The `minValue` and `maxValue` props can be used to limit the entered value to a specific range. The value will be clamped when the user blurs the input field. In addition, the increment and decrement buttons will be disabled when the value is within one `step` value from the bounds (see below for info about steps). Ranges can be open ended by only providing either `minValue` or `maxValue` rather than both. If a valid range is known ahead of time, it is a good idea to provide it to `NumberField` so it can optimize the experience. For example, when the minimum value is greater than or equal to zero, it is possible to use a numeric keyboard on iOS rather than a full text keyboard (necessary to enter a minus sign). <NumberField label="Enter your age" minValue={0} /> <NumberField label="Enter your age" minValue={0} /> <NumberField label="Enter your age" minValue={0} /> Enter your age \-+ ### Step values# The `step` prop can be used to snap the value to certain increments. If there is a `minValue` defined, the steps are calculated starting from the minimum. For example, if `minValue={2}`, and `step={3}`, the valid step values would be 2, 5, 8, 11, etc. If no `minValue` is defined, the steps are calculated starting from zero and extending in both directions. In other words, such that the values are evenly divisible by the step. If no `step` is defined, any decimal value may be typed, but incrementing and decrementing snaps the value to an integer. If the user types a value that is between two steps and blurs the input, the value will be snapped to the nearest step. When incrementing or decrementing, the value is snapped to the nearest step that is higher or lower, respectively. When incrementing or decrementing from an empty field, the value starts at the `minValue` or `maxValue`, respectively, if defined. Otherwise, the value starts from `0`. <NumberField label="Step" step={10} /> <NumberField label="Step + minValue" minValue={2} step={3} /> <NumberField label="Step + minValue + maxValue" minValue={2} maxValue={21} step={3} /> <NumberField label="Step" step={10} /> <NumberField label="Step + minValue" minValue={2} step={3} /> <NumberField label="Step + minValue + maxValue" minValue={2} maxValue={21} step={3} /> <NumberField label="Step" step={10} /> <NumberField label="Step + minValue" minValue={2} step={3} /> <NumberField label="Step + minValue + maxValue" minValue={2} maxValue={21} step={3} /> Step \-+ Step + minValue \-+ Step + minValue + maxValue \-+ ### Disabled and read only# The `isDisabled` and `isReadOnly` props can be used prevent the user from editing the value of the number field. The difference is that `isReadOnly` still allows the input to be focused, while `isDisabled` prevents all user interaction. <NumberField label="Disabled" isDisabled value={25} /> <NumberField label="Read only" isReadOnly value={32} /> <NumberField label="Disabled" isDisabled value={25} /> <NumberField label="Read only" isReadOnly value={32} /> <NumberField label="Disabled" isDisabled value={25} /> <NumberField label="Read only" isReadOnly value={32} /> Disabled \-+ Read only \-+ ## Internationalization# * * * `useNumberField` handles many aspects of internationalization automatically, including formatting and parsing numbers according to the current locale and numbering system. In addition, the increment and decrement buttons have localized ARIA labels. You are responsible for localizing the label text passed into the number field. ### RTL# In right-to-left languages, the number field should be mirrored. The order of the input and buttons should be flipped, and the input text should be right aligned instead of left aligned. Ensure that your CSS accounts for this. --- ## Page: https://react-spectrum.adobe.com/react-aria/useRadioGroup.html # useRadioGroup Provides the behavior and accessibility implementation for a radio group component. Radio groups allow users to select a single item from a list of mutually exclusive options. <table><tbody><tr><th>install</th><td><code>yarn add react-aria</code></td></tr><tr><th>version</th><td>3.41.1</td></tr><tr><th>usage</th><td><code><span>import</span> {useRadioGroup, useRadio} <span>from</span> <span>'react-aria'</span></code></td></tr></tbody></table> View ARIA pattern W3C View repository GitHub View package NPM ## API# * * * `useRadioGroup( (props: AriaRadioGroupProps , , state: RadioGroupState )): RadioGroupAria ``useRadio( props: AriaRadioProps , state: RadioGroupState , ref: RefObject <HTMLInputElement | | null> ): RadioAria ` ## Features# * * * Radio groups can be built in HTML with the <fieldset> and <input> elements, however these can be difficult to style. `useRadioGroup` and `useRadio` help achieve accessible radio groups that can be styled as needed. * Radio groups are exposed to assistive technology via ARIA * Each radio is built with a native HTML `<input>` element, which can be optionally visually hidden to allow custom styling * Full support for browser features like form autofill and validation * Keyboard focus management and cross browser normalization * Group and radio labeling support for assistive technology ## Anatomy# * * * A radio group consists of a set of radio buttons, and a label. Each radio includes a label and a visual selection indicator. A single radio button within the group can be selected at a time. Users may click or touch a radio button to select it, or use the Tab key to navigate to the group, the arrow keys to navigate within the group, and the Space key to select an option. `useRadioGroup` returns props for the group and its label, which you should spread onto the appropriate element: | Name | Type | Description | | --- | --- | --- | | `radioGroupProps` | `DOMAttributes` | Props for the radio group wrapper element. | | `labelProps` | `DOMAttributes` | Props for the radio group's visible label (if any). | | `descriptionProps` | `DOMAttributes` | Props for the radio group description element, if any. | | `errorMessageProps` | `DOMAttributes` | Props for the radio group error message element, if any. | | `isInvalid` | `boolean` | Whether the input value is invalid. | | `validationErrors` | `string[]` | The current error messages for the input if it is invalid, otherwise an empty array. | | `validationDetails` | `ValidityState` | The native validation details for the input. | `useRadio` returns props for an individual radio, along with states that can be used for styling: | Name | Type | Description | | --- | --- | --- | | `labelProps` | `LabelHTMLAttributes<HTMLLabelElement>` | Props for the label wrapper element. | | `inputProps` | `InputHTMLAttributes<HTMLInputElement>` | Props for the input element. | | `isDisabled` | `boolean` | Whether the radio is disabled. | | `isSelected` | `boolean` | Whether the radio is currently selected. | | `isPressed` | `boolean` | Whether the radio is in a pressed state. | Selection state is managed by the `useRadioGroupState` hook in `@react-stately/radio`. The state object should be passed as an option to `useRadio`. Individual radio buttons must have a visual label. If the radio group does not have a visible label, an `aria-label` or `aria-labelledby` prop must be passed instead to identify the element to assistive technology. ## Example# * * * This example uses native input elements for the radios, and React context to share state from the group to each radio. An HTML `<label>` element wraps the native input and the text to provide an implicit label for the radio. import {useRadioGroupState} from 'react-stately'; import {useRadio, useRadioGroup} from 'react-aria'; let RadioContext = React.createContext(null); function RadioGroup(props) { let { children, label, description, errorMessage } = props; let state = useRadioGroupState(props); let { radioGroupProps, labelProps, descriptionProps, errorMessageProps } = useRadioGroup(props, state); return ( <div {...radioGroupProps}> <span {...labelProps}>{label}</span> <RadioContext.Provider value={state}> {children} </RadioContext.Provider> {description && ( <div {...descriptionProps} style={{ fontSize: 12 }}>{description}</div> )} {errorMessage && state.isInvalid && ( <div {...errorMessageProps} style={{ color: 'red', fontSize: 12 }}> {errorMessage} </div> )} </div> ); } function Radio(props) { let { children } = props; let state = React.useContext(RadioContext); let ref = React.useRef(null); let { inputProps } = useRadio(props, state, ref); return ( <label style={{ display: 'block' }}> <input {...inputProps} ref={ref} /> {children} </label> ); } <RadioGroup label="Favorite pet"> <Radio value="dogs">Dogs</Radio> <Radio value="cats">Cats</Radio> </RadioGroup> import {useRadioGroupState} from 'react-stately'; import {useRadio, useRadioGroup} from 'react-aria'; let RadioContext = React.createContext(null); function RadioGroup(props) { let { children, label, description, errorMessage } = props; let state = useRadioGroupState(props); let { radioGroupProps, labelProps, descriptionProps, errorMessageProps } = useRadioGroup(props, state); return ( <div {...radioGroupProps}> <span {...labelProps}>{label}</span> <RadioContext.Provider value={state}> {children} </RadioContext.Provider> {description && ( <div {...descriptionProps} style={{ fontSize: 12 }}> {description} </div> )} {errorMessage && state.isInvalid && ( <div {...errorMessageProps} style={{ color: 'red', fontSize: 12 }} > {errorMessage} </div> )} </div> ); } function Radio(props) { let { children } = props; let state = React.useContext(RadioContext); let ref = React.useRef(null); let { inputProps } = useRadio(props, state, ref); return ( <label style={{ display: 'block' }}> <input {...inputProps} ref={ref} /> {children} </label> ); } <RadioGroup label="Favorite pet"> <Radio value="dogs">Dogs</Radio> <Radio value="cats">Cats</Radio> </RadioGroup> import {useRadioGroupState} from 'react-stately'; import { useRadio, useRadioGroup } from 'react-aria'; let RadioContext = React .createContext(null); function RadioGroup( props ) { let { children, label, description, errorMessage } = props; let state = useRadioGroupState( props ); let { radioGroupProps, labelProps, descriptionProps, errorMessageProps } = useRadioGroup( props, state ); return ( <div {...radioGroupProps} > <span {...labelProps} > {label} </span> <RadioContext.Provider value={state} > {children} </RadioContext.Provider> {description && ( <div {...descriptionProps} style={{ fontSize: 12 }} > {description} </div> )} {errorMessage && state .isInvalid && ( <div {...errorMessageProps} style={{ color: 'red', fontSize: 12 }} > {errorMessage} </div> )} </div> ); } function Radio(props) { let { children } = props; let state = React .useContext( RadioContext ); let ref = React.useRef( null ); let { inputProps } = useRadio( props, state, ref ); return ( <label style={{ display: 'block' }} > <input {...inputProps} ref={ref} /> {children} </label> ); } <RadioGroup label="Favorite pet"> <Radio value="dogs"> Dogs </Radio> <Radio value="cats"> Cats </Radio> </RadioGroup> Favorite petDogsCats ## Styling# * * * To build a custom styled radio button, you can make the native input element visually hidden. This is possible using the < `VisuallyHidden` \> utility component from `@react-aria/visually-hidden`. It is still in the DOM and accessible to assistive technology, but invisible. This example uses SVG to build the visual radio button, which is hidden from screen readers with `aria-hidden`. For keyboard accessibility, a focus ring is important to indicate which element has keyboard focus. This is implemented with the `useFocusRing` hook from `@react-aria/focus`. When `isFocusVisible` is true, an extra SVG element is rendered to indicate focus. The focus ring is only visible when the user is interacting with a keyboard, not with a mouse or touch. import {useFocusRing, VisuallyHidden} from 'react-aria'; // RadioGroup is the same as in the previous example let RadioContext = React.createContext(null); function RadioGroup(props) { let { children, label, description } = props; let state = useRadioGroupState(props); let { radioGroupProps, labelProps, descriptionProps, errorMessageProps, isInvalid, validationErrors } = useRadioGroup(props, state); return ( <div {...radioGroupProps}> <span {...labelProps}>{label}</span> <RadioContext.Provider value={state}> {children} </RadioContext.Provider> {description && ( <div {...descriptionProps} style={{ fontSize: 12 }}>{description}</div> )} {isInvalid && ( <div {...errorMessageProps} style={{ color: 'red', fontSize: 12 }}> {validationErrors.join(' ')} </div> )} </div> ); } function Radio(props) { let { children } = props; let state = React.useContext(RadioContext); let ref = React.useRef(null); let { inputProps, isSelected, isDisabled } = useRadio(props, state, ref); let { isFocusVisible, focusProps } = useFocusRing(); let strokeWidth = isSelected ? 6 : 2; return ( <label style={{ display: 'flex', alignItems: 'center', opacity: isDisabled ? 0.4 : 1 }} > <VisuallyHidden> <input {...inputProps} {...focusProps} ref={ref} /> </VisuallyHidden> <svg width={24} height={24} aria-hidden="true" style={{ marginRight: 4 }} > <circle cx={12} cy={12} r={8 - strokeWidth / 2} fill="none" stroke={isSelected ? 'orange' : 'gray'} strokeWidth={strokeWidth} /> {isFocusVisible && ( <circle cx={12} cy={12} r={11} fill="none" stroke="orange" strokeWidth={2} /> )} </svg> {children} </label> ); } <RadioGroup label="Favorite pet"> <Radio value="dogs">Dogs</Radio> <Radio value="cats">Cats</Radio> </RadioGroup> import {useFocusRing, VisuallyHidden} from 'react-aria'; // RadioGroup is the same as in the previous example let RadioContext = React.createContext(null); function RadioGroup(props) { let { children, label, description } = props; let state = useRadioGroupState(props); let { radioGroupProps, labelProps, descriptionProps, errorMessageProps, isInvalid, validationErrors } = useRadioGroup(props, state); return ( <div {...radioGroupProps}> <span {...labelProps}>{label}</span> <RadioContext.Provider value={state}> {children} </RadioContext.Provider> {description && ( <div {...descriptionProps} style={{ fontSize: 12 }}> {description} </div> )} {isInvalid && ( <div {...errorMessageProps} style={{ color: 'red', fontSize: 12 }} > {validationErrors.join(' ')} </div> )} </div> ); } function Radio(props) { let { children } = props; let state = React.useContext(RadioContext); let ref = React.useRef(null); let { inputProps, isSelected, isDisabled } = useRadio( props, state, ref ); let { isFocusVisible, focusProps } = useFocusRing(); let strokeWidth = isSelected ? 6 : 2; return ( <label style={{ display: 'flex', alignItems: 'center', opacity: isDisabled ? 0.4 : 1 }} > <VisuallyHidden> <input {...inputProps} {...focusProps} ref={ref} /> </VisuallyHidden> <svg width={24} height={24} aria-hidden="true" style={{ marginRight: 4 }} > <circle cx={12} cy={12} r={8 - strokeWidth / 2} fill="none" stroke={isSelected ? 'orange' : 'gray'} strokeWidth={strokeWidth} /> {isFocusVisible && ( <circle cx={12} cy={12} r={11} fill="none" stroke="orange" strokeWidth={2} /> )} </svg> {children} </label> ); } <RadioGroup label="Favorite pet"> <Radio value="dogs">Dogs</Radio> <Radio value="cats">Cats</Radio> </RadioGroup> import { useFocusRing, VisuallyHidden } from 'react-aria'; // RadioGroup is the same as in the previous example let RadioContext = React .createContext(null); function RadioGroup( props ) { let { children, label, description } = props; let state = useRadioGroupState( props ); let { radioGroupProps, labelProps, descriptionProps, errorMessageProps, isInvalid, validationErrors } = useRadioGroup( props, state ); return ( <div {...radioGroupProps} > <span {...labelProps} > {label} </span> <RadioContext.Provider value={state} > {children} </RadioContext.Provider> {description && ( <div {...descriptionProps} style={{ fontSize: 12 }} > {description} </div> )} {isInvalid && ( <div {...errorMessageProps} style={{ color: 'red', fontSize: 12 }} > {validationErrors .join(' ')} </div> )} </div> ); } function Radio(props) { let { children } = props; let state = React .useContext( RadioContext ); let ref = React.useRef( null ); let { inputProps, isSelected, isDisabled } = useRadio( props, state, ref ); let { isFocusVisible, focusProps } = useFocusRing(); let strokeWidth = isSelected ? 6 : 2; return ( <label style={{ display: 'flex', alignItems: 'center', opacity: isDisabled ? 0.4 : 1 }} > <VisuallyHidden> <input {...inputProps} {...focusProps} ref={ref} /> </VisuallyHidden> <svg width={24} height={24} aria-hidden="true" style={{ marginRight: 4 }} > <circle cx={12} cy={12} r={8 - strokeWidth / 2} fill="none" stroke={isSelected ? 'orange' : 'gray'} strokeWidth={strokeWidth} /> {isFocusVisible && ( <circle cx={12} cy={12} r={11} fill="none" stroke="orange" strokeWidth={2} /> )} </svg> {children} </label> ); } <RadioGroup label="Favorite pet"> <Radio value="dogs"> Dogs </Radio> <Radio value="cats"> Cats </Radio> </RadioGroup> Favorite pet Dogs Cats ## Styled examples# * * * Swatch Group A color swatch picker built with Tailwind CSS. Selectable Cards A selectable card group built with Styled Components. Button Group A single-selectable segmented button group. ## Usage# * * * The following examples show how to use the `RadioGroup` component created in the above example. ### Default value# An initial, uncontrolled value can be provided to the RadioGroup using the `defaultValue` prop, which accepts a value corresponding with the `value` prop of each Radio. <RadioGroup label="Are you a wizard?" defaultValue="yes"> <Radio value="yes">Yes</Radio> <Radio value="no">No</Radio> </RadioGroup> <RadioGroup label="Are you a wizard?" defaultValue="yes"> <Radio value="yes">Yes</Radio> <Radio value="no">No</Radio> </RadioGroup> <RadioGroup label="Are you a wizard?" defaultValue="yes" > <Radio value="yes"> Yes </Radio> <Radio value="no"> No </Radio> </RadioGroup> Are you a wizard? Yes No ### Controlled value# A controlled value can be provided using the `value` prop, which accepts a value corresponding with the `value` prop of each Radio. The `onChange` event is fired when the user selects a radio. function Example() { let [selected, setSelected] = React.useState(null); return ( <> <RadioGroup label="Favorite avatar" value={selected} onChange={setSelected} > <Radio value="wizard">Wizard</Radio> <Radio value="dragon">Dragon</Radio> </RadioGroup> <p>You have selected: {selected}</p> </> ); } function Example() { let [selected, setSelected] = React.useState(null); return ( <> <RadioGroup label="Favorite avatar" value={selected} onChange={setSelected} > <Radio value="wizard">Wizard</Radio> <Radio value="dragon">Dragon</Radio> </RadioGroup> <p>You have selected: {selected}</p> </> ); } function Example() { let [ selected, setSelected ] = React.useState( null ); return ( <> <RadioGroup label="Favorite avatar" value={selected} onChange={setSelected} > <Radio value="wizard"> Wizard </Radio> <Radio value="dragon"> Dragon </Radio> </RadioGroup> <p> You have selected:{' '} {selected} </p> </> ); } Favorite avatar Wizard Dragon You have selected: ### Description# The `description` prop can be used to associate additional help text with a radio group. <RadioGroup label="Favorite pet" description="Select your favorite pet."> <Radio value="dogs">Dogs</Radio> <Radio value="cats">Cats</Radio> </RadioGroup> <RadioGroup label="Favorite pet" description="Select your favorite pet." > <Radio value="dogs">Dogs</Radio> <Radio value="cats">Cats</Radio> </RadioGroup> <RadioGroup label="Favorite pet" description="Select your favorite pet." > <Radio value="dogs"> Dogs </Radio> <Radio value="cats"> Cats </Radio> </RadioGroup> Favorite pet Dogs Cats Select your favorite pet. ### Validation# RadioGroup supports the `isRequired` prop to ensure the user selects an option, as well as custom client and server-side validation. It can also be integrated with other form libraries. See the Forms guide to learn more. When a RadioGroup has the `validationBehavior="native"` prop, validation errors block form submission. To display validation errors, use the `validationErrors` and `errorMessageProps` returned by `useRadioGroup`. This allows you to render error messages from all of the above sources with consistent custom styles. <form> <RadioGroup label="Favorite pet" name="pet" isRequired validationBehavior="native" > <Radio value="dogs">Dog</Radio> <Radio value="cats">Cat</Radio> <Radio value="dragon">Dragon</Radio> </RadioGroup> <input type="submit" style={{ marginTop: 8 }} /> </form> <form> <RadioGroup label="Favorite pet" name="pet" isRequired validationBehavior="native" > <Radio value="dogs">Dog</Radio> <Radio value="cats">Cat</Radio> <Radio value="dragon">Dragon</Radio> </RadioGroup> <input type="submit" style={{ marginTop: 8 }} /> </form> <form> <RadioGroup label="Favorite pet" name="pet" isRequired validationBehavior="native" > <Radio value="dogs"> Dog </Radio> <Radio value="cats"> Cat </Radio> <Radio value="dragon"> Dragon </Radio> </RadioGroup> <input type="submit" style={{ marginTop: 8 }} /> </form> Favorite pet Dog Cat Dragon ### Disabled# The entire RadioGroup can be disabled with the `isDisabled` prop. <RadioGroup label="Favorite sport" isDisabled> <Radio value="soccer">Soccer</Radio> <Radio value="baseball">Baseball</Radio> <Radio value="basketball">Basketball</Radio> </RadioGroup> <RadioGroup label="Favorite sport" isDisabled> <Radio value="soccer">Soccer</Radio> <Radio value="baseball">Baseball</Radio> <Radio value="basketball">Basketball</Radio> </RadioGroup> <RadioGroup label="Favorite sport" isDisabled > <Radio value="soccer"> Soccer </Radio> <Radio value="baseball"> Baseball </Radio> <Radio value="basketball"> Basketball </Radio> </RadioGroup> Favorite sport Soccer Baseball Basketball To disable an individual radio, pass `isDisabled` to the `Radio` instead. <RadioGroup label="Favorite sport"> <Radio value="soccer">Soccer</Radio> <Radio value="baseball" isDisabled>Baseball</Radio> <Radio value="basketball">Basketball</Radio> </RadioGroup> <RadioGroup label="Favorite sport"> <Radio value="soccer">Soccer</Radio> <Radio value="baseball" isDisabled>Baseball</Radio> <Radio value="basketball">Basketball</Radio> </RadioGroup> <RadioGroup label="Favorite sport"> <Radio value="soccer"> Soccer </Radio> <Radio value="baseball" isDisabled > Baseball </Radio> <Radio value="basketball"> Basketball </Radio> </RadioGroup> Favorite sport Soccer Baseball Basketball ### Read only# The `isReadOnly` prop makes the selection immutable. Unlike `isDisabled`, the RadioGroup remains focusable. See the MDN docs for more information. <RadioGroup label="Favorite avatar" defaultValue="wizard" isReadOnly> <Radio value="wizard">Wizard</Radio> <Radio value="dragon">Dragon</Radio> </RadioGroup> <RadioGroup label="Favorite avatar" defaultValue="wizard" isReadOnly > <Radio value="wizard">Wizard</Radio> <Radio value="dragon">Dragon</Radio> </RadioGroup> <RadioGroup label="Favorite avatar" defaultValue="wizard" isReadOnly > <Radio value="wizard"> Wizard </Radio> <Radio value="dragon"> Dragon </Radio> </RadioGroup> Favorite avatar Wizard Dragon ### HTML forms# RadioGroup supports the `name` prop, paired with the Radio `value` prop, for integration with HTML forms. <RadioGroup label="Favorite pet" name="pet"> <Radio value="dogs">Dogs</Radio> <Radio value="cats">Cats</Radio> </RadioGroup> <RadioGroup label="Favorite pet" name="pet"> <Radio value="dogs">Dogs</Radio> <Radio value="cats">Cats</Radio> </RadioGroup> <RadioGroup label="Favorite pet" name="pet" > <Radio value="dogs"> Dogs </Radio> <Radio value="cats"> Cats </Radio> </RadioGroup> Favorite pet Dogs Cats ## Internationalization# * * * ### RTL# In right-to-left languages, the radio group and radio buttons should be mirrored. The group should be right-aligned, and the radio should be placed on the right side of the label. Ensure that your CSS accounts for this. --- ## Page: https://react-spectrum.adobe.com/react-aria/useSearchField.html # useSearchField Provides the behavior and accessibility implementation for a search field. <table><tbody><tr><th>install</th><td><code>yarn add react-aria</code></td></tr><tr><th>version</th><td>3.41.1</td></tr><tr><th>usage</th><td><code><span>import</span> {useSearchField} <span>from</span> <span>'react-aria'</span></code></td></tr></tbody></table> View ARIA pattern W3C View repository GitHub View package NPM ## API# * * * `useSearchField( props: AriaSearchFieldProps , state: SearchFieldState , inputRef: RefObject <HTMLInputElement | | null> ): SearchFieldAria ` ## Features# * * * Search fields can be built with `<input type="search">`, but these can be hard to style consistently cross browser. `useSearchField` helps achieve accessible search fields that can be styled as needed. * Built with a native `<input type="search">` element * Visual and ARIA labeling support * Keyboard submit handling via the Enter key * Keyboard support for clearing the search field with the Escape key * Custom clear button support with internationalized label for accessibility * Support for native HTML constraint validation with customizable UI, custom validation functions, realtime validation, and server-side validation errors ## Anatomy# * * * Search fields consist of an input element, a label, and an optional clear button. `useSearchField` automatically manages the labeling and relationships between the elements, and handles keyboard events. Users can press the Escape key to clear the search field, or the Enter key to trigger the `onSubmit` event. `useSearchField` also supports optional description and error message elements, which can be used to provide more context about the field, and any validation messages. These are linked with the input via the `aria-describedby` attribute. `useSearchField` returns props that you should spread onto the appropriate elements: | Name | Type | Description | | --- | --- | --- | | `labelProps` | `LabelHTMLAttributes<HTMLLabelElement>` | Props for the text field's visible label element (if any). | | `inputProps` | `InputHTMLAttributes<HTMLInputElement>` | Props for the input element. | | `clearButtonProps` | ` AriaButtonProps ` | Props for the clear button. | | `descriptionProps` | `DOMAttributes` | Props for the searchfield's description element, if any. | | `errorMessageProps` | `DOMAttributes` | Props for the searchfield's error message element, if any. | | `isInvalid` | `boolean` | Whether the input value is invalid. | | `validationErrors` | `string[]` | The current error messages for the input if it is invalid, otherwise an empty array. | | `validationDetails` | `ValidityState` | The native validation details for the input. | State is managed by the `useSearchFieldState` hook in `@react-stately/searchfield`. The state object should be passed as an option to `useSearchField`. If there is no visual label, an `aria-label` or `aria-labelledby` prop must be passed instead to identify the element to screen readers. ## Example# * * * **Note**: This example does not show the optional description or error message elements. See useTextField for an example of that. import {useSearchFieldState} from 'react-stately'; import {useSearchField} from 'react-aria'; function SearchField(props) { let { label } = props; let state = useSearchFieldState(props); let ref = React.useRef(null); let { labelProps, inputProps } = useSearchField(props, state, ref); return ( <div style={{ display: 'flex', flexDirection: 'column', width: 200 }}> <label {...labelProps}>{label}</label> <input {...inputProps} ref={ref} /> </div> ); } <SearchField label="Search" onSubmit={(text) => alert(text)} /> import {useSearchFieldState} from 'react-stately'; import {useSearchField} from 'react-aria'; function SearchField(props) { let { label } = props; let state = useSearchFieldState(props); let ref = React.useRef(null); let { labelProps, inputProps } = useSearchField( props, state, ref ); return ( <div style={{ display: 'flex', flexDirection: 'column', width: 200 }} > <label {...labelProps}>{label}</label> <input {...inputProps} ref={ref} /> </div> ); } <SearchField label="Search" onSubmit={(text) => alert(text)} /> import {useSearchFieldState} from 'react-stately'; import {useSearchField} from 'react-aria'; function SearchField( props ) { let { label } = props; let state = useSearchFieldState( props ); let ref = React.useRef( null ); let { labelProps, inputProps } = useSearchField( props, state, ref ); return ( <div style={{ display: 'flex', flexDirection: 'column', width: 200 }} > <label {...labelProps} > {label} </label> <input {...inputProps} ref={ref} /> </div> ); } <SearchField label="Search" onSubmit={(text) => alert(text)} /> Search ## Styling# * * * This example uses CSS to reset the default browser styling for a search field and implement custom styles. It also includes a custom clear button, built with useButton. The `Button` component is independent, and can be shared with many other components. // Reuse the Button from your component library. See below for details. import {Button} from 'your-component-library'; function SearchField(props) { let { label } = props; let state = useSearchFieldState(props); let ref = React.useRef(null); let { labelProps, inputProps, clearButtonProps } = useSearchField( props, state, ref ); return ( <div className="search-field"> <label {...labelProps}>{label}</label> <div> <input {...inputProps} ref={ref} /> {state.value !== '' && <Button {...clearButtonProps}>❎</Button>} </div> </div> ); } <SearchField label="Search" onSubmit={(text) => alert(text)} /> // Reuse the Button from your component library. See below for details. import {Button} from 'your-component-library'; function SearchField(props) { let { label } = props; let state = useSearchFieldState(props); let ref = React.useRef(null); let { labelProps, inputProps, clearButtonProps } = useSearchField(props, state, ref); return ( <div className="search-field"> <label {...labelProps}>{label}</label> <div> <input {...inputProps} ref={ref} /> {state.value !== '' && <Button {...clearButtonProps}>❎</Button>} </div> </div> ); } <SearchField label="Search" onSubmit={(text) => alert(text)} /> // Reuse the Button from your component library. See below for details. import {Button} from 'your-component-library'; function SearchField( props ) { let { label } = props; let state = useSearchFieldState( props ); let ref = React.useRef( null ); let { labelProps, inputProps, clearButtonProps } = useSearchField( props, state, ref ); return ( <div className="search-field"> <label {...labelProps} > {label} </label> <div> <input {...inputProps} ref={ref} /> {state.value !== '' && ( <Button {...clearButtonProps} > ❎ </Button> )} </div> </div> ); } <SearchField label="Search" onSubmit={(text) => alert(text)} /> Search Show CSS /* css */ .search-field { display: flex; flex-direction: column; } .search-field div { background: slategray; padding: 4px 0 4px 4px; border-radius: 4px; width: 250px; display: flex; } .search-field input { flex: 1; color: white; font-size: 15px; padding: 2px 0; } .search-field input, .search-field button { -webkit-appearance: none; border: none; outline: none; background: none; } .search-field input::-webkit-search-cancel-button, .search-field input::-webkit-search-decoration { -webkit-appearance: none; } /* css */ .search-field { display: flex; flex-direction: column; } .search-field div { background: slategray; padding: 4px 0 4px 4px; border-radius: 4px; width: 250px; display: flex; } .search-field input { flex: 1; color: white; font-size: 15px; padding: 2px 0; } .search-field input, .search-field button { -webkit-appearance: none; border: none; outline: none; background: none; } .search-field input::-webkit-search-cancel-button, .search-field input::-webkit-search-decoration { -webkit-appearance: none; } /* css */ .search-field { display: flex; flex-direction: column; } .search-field div { background: slategray; padding: 4px 0 4px 4px; border-radius: 4px; width: 250px; display: flex; } .search-field input { flex: 1; color: white; font-size: 15px; padding: 2px 0; } .search-field input, .search-field button { -webkit-appearance: none; border: none; outline: none; background: none; } .search-field input::-webkit-search-cancel-button, .search-field input::-webkit-search-decoration { -webkit-appearance: none; } ### Button# The `Button` component is used in the above example to clear the search field. It is built using the useButton hook, and can be shared with many other components. Show code import {useButton} from 'react-aria'; function Button(props) { let ref = React.useRef(null); let { buttonProps } = useButton(props, ref); return <button {...buttonProps} ref={ref}>{props.children}</button>; } import {useButton} from 'react-aria'; function Button(props) { let ref = React.useRef(null); let { buttonProps } = useButton(props, ref); return ( <button {...buttonProps} ref={ref}> {props.children} </button> ); } import {useButton} from 'react-aria'; function Button(props) { let ref = React.useRef( null ); let { buttonProps } = useButton( props, ref ); return ( <button {...buttonProps} ref={ref} > {props.children} </button> ); } ## Usage# * * * The following examples show how to use the `SearchField` component created in the above example. ### Default value# A SearchField's `value` is empty by default, but an initial, uncontrolled, value can be provided using the `defaultValue` prop. <SearchField label="Search" defaultValue="Puppies" /> <SearchField label="Search" defaultValue="Puppies" /> <SearchField label="Search" defaultValue="Puppies" /> Search ❎ ### Controlled value# The `value` prop can be used to make the value controlled. The `onChange` event is fired when the user edits the text, and receives the new value. function Example() { let [text, setText] = React.useState(''); return ( <> <SearchField label="Search" onChange={setText} /> <p>Mirrored text: {text}</p> </> ); } function Example() { let [text, setText] = React.useState(''); return ( <> <SearchField label="Search" onChange={setText} /> <p>Mirrored text: {text}</p> </> ); } function Example() { let [text, setText] = React.useState(''); return ( <> <SearchField label="Search" onChange={setText} /> <p> Mirrored text: {' '} {text} </p> </> ); } Search Mirrored text: ### Events# The most commonly used handlers for events in SearchField are the: * `onChange` prop which is triggered whenever the value is edited by the user. * `onSubmit` prop which is triggered whenever the value is submitted by the user (e.g. by pressing Enter). * `onClear` prop which is triggered whenever the value is cleared by the user (e.g. by pressing clear button or Escape key). The example below uses `onChange`, `onSubmit`, and `onClear` to update two separate elements with the text entered into the SearchField. function Example() { let [currentText, setCurrentText] = React.useState(''); let [submittedText, setSubmittedText] = React.useState(''); return ( <div> <SearchField onClear={() => setCurrentText('')} onChange={setCurrentText} onSubmit={setSubmittedText} label="Your text" value={currentText} /> <p>Mirrored text: {currentText}</p> <p>Submitted text: {submittedText}</p> </div> ); } function Example() { let [currentText, setCurrentText] = React.useState(''); let [submittedText, setSubmittedText] = React.useState( '' ); return ( <div> <SearchField onClear={() => setCurrentText('')} onChange={setCurrentText} onSubmit={setSubmittedText} label="Your text" value={currentText} /> <p>Mirrored text: {currentText}</p> <p>Submitted text: {submittedText}</p> </div> ); } function Example() { let [ currentText, setCurrentText ] = React.useState(''); let [ submittedText, setSubmittedText ] = React.useState(''); return ( <div> <SearchField onClear={() => setCurrentText( '' )} onChange={setCurrentText} onSubmit={setSubmittedText} label="Your text" value={currentText} /> <p> Mirrored text: {' '} {currentText} </p> <p> Submitted text: {' '} {submittedText} </p> </div> ); } Your text Mirrored text: Submitted text: ### Disabled# A SearchField can be disabled using the `isDisabled` prop. <SearchField label="Email" isDisabled /> <SearchField label="Email" isDisabled /> <SearchField label="Email" isDisabled /> Email ### Read only# The `isReadOnly` boolean prop makes the SearchField's text content immutable. Unlike `isDisabled`, the SearchField remains focusable and the contents can still be copied. See the MDN docs for more information. <SearchField label="Email" defaultValue="abc@adobe.com" isReadOnly /> <SearchField label="Email" defaultValue="abc@adobe.com" isReadOnly /> <SearchField label="Email" defaultValue="abc@adobe.com" isReadOnly /> Email ❎ ### HTML forms# SearchField supports the `name` prop for integration with HTML forms. In addition, attributes such as `type`, `pattern`, `inputMode`, and others are passed through to the underlying `<input>` element. <SearchField label="Email" name="email" type="email" /> <SearchField label="Email" name="email" type="email" /> <SearchField label="Email" name="email" type="email" /> Email ## Internationalization# * * * ### RTL# In right-to-left languages, search fields should be mirrored. The label should be right aligned, along with the text in the input. Ensure that your CSS accounts for this. --- ## Page: https://react-spectrum.adobe.com/react-aria/useSlider.html # useSlider Provides the behavior and accessibility implementation for a slider component representing one or more values. <table><tbody><tr><th>install</th><td><code>yarn add react-aria</code></td></tr><tr><th>version</th><td>3.41.1</td></tr><tr><th>usage</th><td><code><span>import</span> {useSlider, useSliderThumb} <span>from</span> <span>'react-aria'</span></code></td></tr></tbody></table> View ARIA pattern W3C View repository GitHub View package NPM ## API# * * * `useSlider<T extends number | number[]>( props: AriaSliderProps <T>, state: SliderState , trackRef: RefObject <Element | | null> ): SliderAria ``useSliderThumb( (opts: AriaSliderThumbOptions , , state: SliderState )): SliderThumbAria ` ## Features# * * * The <input type="range"> HTML element can be used to build a slider, however it is very difficult to style cross browser. `useSlider` and `useSliderThumb` help achieve accessible sliders that can be styled as needed. * Support for one or multiple thumbs * Support for mouse, touch, and keyboard via the useMove hook * Multi-touch support for dragging multiple thumbs or multiple sliders at once * Pressing on the track moves the nearest thumb to that position * Supports using the arrow keys, as well as page up/down, home, and end keys * Support for both horizontal and vertical orientations * Support for custom min, max, and step values with handling for rounding errors * Support for disabling the whole slider or individual thumbs * Prevents text selection while dragging * Exposed to assistive technology as a `group` of `slider` elements via ARIA * Slider thumbs use hidden native input elements to support touch screen readers * Support for labeling both the slider as a whole and individual thumbs * Support for displaying the current thumb values using an `<output>` element * Internationalized number formatting as a percentage or value * Support for mirroring in RTL locales ## Anatomy# * * * Sliders consist of a track element showing the range of available values, one or more thumbs showing the current values, an optional <output> element displaying the current values textually, and a label. The thumbs can be dragged to allow a user to change their value. In addition, the track can be clicked to move the nearest thumb to that position. ### useSlider hook# `useSlider` returns several sets of props and you should spread each one onto the appropriate element: | Name | Type | Description | | --- | --- | --- | | `labelProps` | `LabelHTMLAttributes<HTMLLabelElement>` | Props for the label element. | | `groupProps` | `DOMAttributes` | Props for the root element of the slider component; groups slider inputs. | | `trackProps` | `DOMAttributes` | Props for the track element. | | `outputProps` | `OutputHTMLAttributes<HTMLOutputElement>` | Props for the output element, displaying the value of the slider thumbs. | If there is no visual label, an `aria-label` or `aria-labelledby` prop must be passed instead to identify the element to screen readers. ### useSliderThumb hook# `useSliderThumb` returns props that you should spread onto the appropriate elements, along with states for styling: | Name | Type | Description | | --- | --- | --- | | `thumbProps` | `DOMAttributes` | Props for the root thumb element; handles the dragging motion. | | `inputProps` | `InputHTMLAttributes<HTMLInputElement>` | Props for the visually hidden range input element. | | `labelProps` | `LabelHTMLAttributes<HTMLLabelElement>` | Props for the label element for this thumb (optional). | | `isDragging` | `boolean` | Whether this thumb is currently being dragged. | | `isFocused` | `boolean` | Whether the thumb is currently focused. | | `isDisabled` | `boolean` | Whether the thumb is disabled. | If there is no visual label, an `aria-label` or `aria-labelledby` prop must be passed instead to identify each thumb to screen readers. Slider state is managed by the `useSliderState` hook. ## Examples# * * * ### Single thumb# This example shows how to build a simple horizontal slider with a single thumb. In addition, it includes a label which can be clicked to focus the slider thumb, and an `<output>` element to display the current slider value as text. This is formatted using a locale aware number formatter provided by the useNumberFormatter hook. The `<input>` element inside the thumb is used to represent the slider to assistive technology, and is hidden from view using the VisuallyHidden component. The thumb also uses the useFocusRing hook to display using a different color when it is keyboard focused (try tabbing to it). import {useSliderState} from 'react-stately'; import {mergeProps, useFocusRing, useNumberFormatter, useSlider, useSliderThumb, VisuallyHidden} from 'react-aria'; function Slider(props) { let trackRef = React.useRef(null); let numberFormatter = useNumberFormatter(props.formatOptions); let state = useSliderState({ ...props, numberFormatter }); let { groupProps, trackProps, labelProps, outputProps } = useSlider(props, state, trackRef); return ( <div {...groupProps} className={`slider ${state.orientation}`}> {/* Create a container for the label and output element. */} {props.label && ( <div className="label-container"> <label {...labelProps}>{props.label}</label> <output {...outputProps}> {state.getThumbValueLabel(0)} </output> </div> )} {/* The track element holds the visible track line and the thumb. */} <div {...trackProps} ref={trackRef} className={`track ${state.isDisabled ? 'disabled' : ''}`} > <Thumb index={0} state={state} trackRef={trackRef} name={props.name} /> </div> </div> ); } function Thumb(props) { let { state, trackRef, index, name } = props; let inputRef = React.useRef(null); let { thumbProps, inputProps, isDragging } = useSliderThumb({ index, trackRef, inputRef, name }, state); let { focusProps, isFocusVisible } = useFocusRing(); return ( <div {...thumbProps} className={`thumb ${isFocusVisible ? 'focus' : ''} ${ isDragging ? 'dragging' : '' }`} > <VisuallyHidden> <input ref={inputRef} {...mergeProps(inputProps, focusProps)} /> </VisuallyHidden> </div> ); } <Slider label="Opacity" /> import {useSliderState} from 'react-stately'; import { mergeProps, useFocusRing, useNumberFormatter, useSlider, useSliderThumb, VisuallyHidden } from 'react-aria'; function Slider(props) { let trackRef = React.useRef(null); let numberFormatter = useNumberFormatter( props.formatOptions ); let state = useSliderState({ ...props, numberFormatter }); let { groupProps, trackProps, labelProps, outputProps } = useSlider(props, state, trackRef); return ( <div {...groupProps} className={`slider ${state.orientation}`} > {/* Create a container for the label and output element. */} {props.label && ( <div className="label-container"> <label {...labelProps}>{props.label}</label> <output {...outputProps}> {state.getThumbValueLabel(0)} </output> </div> )} {/* The track element holds the visible track line and the thumb. */} <div {...trackProps} ref={trackRef} className={`track ${ state.isDisabled ? 'disabled' : '' }`} > <Thumb index={0} state={state} trackRef={trackRef} name={props.name} /> </div> </div> ); } function Thumb(props) { let { state, trackRef, index, name } = props; let inputRef = React.useRef(null); let { thumbProps, inputProps, isDragging } = useSliderThumb({ index, trackRef, inputRef, name }, state); let { focusProps, isFocusVisible } = useFocusRing(); return ( <div {...thumbProps} className={`thumb ${isFocusVisible ? 'focus' : ''} ${ isDragging ? 'dragging' : '' }`} > <VisuallyHidden> <input ref={inputRef} {...mergeProps(inputProps, focusProps)} /> </VisuallyHidden> </div> ); } <Slider label="Opacity" /> import {useSliderState} from 'react-stately'; import { mergeProps, useFocusRing, useNumberFormatter, useSlider, useSliderThumb, VisuallyHidden } from 'react-aria'; function Slider(props) { let trackRef = React .useRef(null); let numberFormatter = useNumberFormatter( props.formatOptions ); let state = useSliderState({ ...props, numberFormatter }); let { groupProps, trackProps, labelProps, outputProps } = useSlider( props, state, trackRef ); return ( <div {...groupProps} className={`slider ${state.orientation}`} > {/* Create a container for the label and output element. */} {props.label && ( <div className="label-container"> <label {...labelProps} > {props .label} </label> <output {...outputProps} > {state .getThumbValueLabel( 0 )} </output> </div> )} {/* The track element holds the visible track line and the thumb. */} <div {...trackProps} ref={trackRef} className={`track ${ state .isDisabled ? 'disabled' : '' }`} > <Thumb index={0} state={state} trackRef={trackRef} name={props .name} /> </div> </div> ); } function Thumb(props) { let { state, trackRef, index, name } = props; let inputRef = React .useRef(null); let { thumbProps, inputProps, isDragging } = useSliderThumb({ index, trackRef, inputRef, name }, state); let { focusProps, isFocusVisible } = useFocusRing(); return ( <div {...thumbProps} className={`thumb ${ isFocusVisible ? 'focus' : '' } ${ isDragging ? 'dragging' : '' }`} > <VisuallyHidden> <input ref={inputRef} {...mergeProps( inputProps, focusProps )} /> </VisuallyHidden> </div> ); } <Slider label="Opacity" /> Opacity 0 Show CSS .slider { display: flex; } .slider.horizontal { flex-direction: column; width: 300px; } .slider.vertical { height: 150px; } .label-container { display: flex; justify-content: space-between; } .slider.horizontal .track { height: 30px; width: 100%; } /* track line */ .track:before { content: attr(x); display: block; position: absolute; background: gray; } .slider.horizontal .track:before { height: 3px; width: 100%; top: 50%; transform: translateY(-50%); } .slider.vertical .track { width: 30px; height: 100%; } .slider.vertical .track:before { width: 3px; height: 100%; left: 50%; transform: translateX(-50%); } .thumb { width: 20px; height: 20px; border-radius: 50%; background: gray; } .thumb.dragging { background: dimgray; } .thumb.focus { background: orange; } .slider.horizontal .thumb { top: 50%; } .slider.vertical .thumb { left: 50%; } .track.disabled { opacity: 0.4; } .slider { display: flex; } .slider.horizontal { flex-direction: column; width: 300px; } .slider.vertical { height: 150px; } .label-container { display: flex; justify-content: space-between; } .slider.horizontal .track { height: 30px; width: 100%; } /* track line */ .track:before { content: attr(x); display: block; position: absolute; background: gray; } .slider.horizontal .track:before { height: 3px; width: 100%; top: 50%; transform: translateY(-50%); } .slider.vertical .track { width: 30px; height: 100%; } .slider.vertical .track:before { width: 3px; height: 100%; left: 50%; transform: translateX(-50%); } .thumb { width: 20px; height: 20px; border-radius: 50%; background: gray; } .thumb.dragging { background: dimgray; } .thumb.focus { background: orange; } .slider.horizontal .thumb { top: 50%; } .slider.vertical .thumb { left: 50%; } .track.disabled { opacity: 0.4; } .slider { display: flex; } .slider.horizontal { flex-direction: column; width: 300px; } .slider.vertical { height: 150px; } .label-container { display: flex; justify-content: space-between; } .slider.horizontal .track { height: 30px; width: 100%; } /* track line */ .track:before { content: attr(x); display: block; position: absolute; background: gray; } .slider.horizontal .track:before { height: 3px; width: 100%; top: 50%; transform: translateY(-50%); } .slider.vertical .track { width: 30px; height: 100%; } .slider.vertical .track:before { width: 3px; height: 100%; left: 50%; transform: translateX(-50%); } .thumb { width: 20px; height: 20px; border-radius: 50%; background: gray; } .thumb.dragging { background: dimgray; } .thumb.focus { background: orange; } .slider.horizontal .thumb { top: 50%; } .slider.vertical .thumb { left: 50%; } .track.disabled { opacity: 0.4; } ### Multi thumb# This example shows how to build a slider with multiple thumbs. The thumb component is the same one shown in the previous example. The main difference in this example is that there are two `<Thumb>` elements rendered with different `index` props. In addition, the `<output>` element uses `state.getThumbValueLabel` for each thumb to display the selected range. function RangeSlider(props) { let trackRef = React.useRef(null); let numberFormatter = useNumberFormatter(props.formatOptions); let state = useSliderState({ ...props, numberFormatter }); let { groupProps, trackProps, labelProps, outputProps } = useSlider(props, state, trackRef); return ( <div {...groupProps} className={`slider ${state.orientation}`}> {props.label && ( <div className="label-container"> <label {...labelProps}>{props.label}</label> <output {...outputProps}> {`${state.getThumbValueLabel(0)} - ${ state.getThumbValueLabel(1) }`} </output> </div> )} <div {...trackProps} ref={trackRef} className={`track ${state.isDisabled ? 'disabled' : ''}`} > <Thumb index={0} state={state} trackRef={trackRef} /> <Thumb index={1} state={state} trackRef={trackRef} /> </div> </div> ); } <RangeSlider label="Price Range" formatOptions={{ style: 'currency', currency: 'USD' }} maxValue={500} defaultValue={[100, 350]} step={10} /> function RangeSlider(props) { let trackRef = React.useRef(null); let numberFormatter = useNumberFormatter( props.formatOptions ); let state = useSliderState({ ...props, numberFormatter }); let { groupProps, trackProps, labelProps, outputProps } = useSlider(props, state, trackRef); return ( <div {...groupProps} className={`slider ${state.orientation}`} > {props.label && ( <div className="label-container"> <label {...labelProps}>{props.label}</label> <output {...outputProps}> {`${state.getThumbValueLabel(0)} - ${ state.getThumbValueLabel(1) }`} </output> </div> )} <div {...trackProps} ref={trackRef} className={`track ${ state.isDisabled ? 'disabled' : '' }`} > <Thumb index={0} state={state} trackRef={trackRef} /> <Thumb index={1} state={state} trackRef={trackRef} /> </div> </div> ); } <RangeSlider label="Price Range" formatOptions={{ style: 'currency', currency: 'USD' }} maxValue={500} defaultValue={[100, 350]} step={10} /> function RangeSlider( props ) { let trackRef = React .useRef(null); let numberFormatter = useNumberFormatter( props.formatOptions ); let state = useSliderState({ ...props, numberFormatter }); let { groupProps, trackProps, labelProps, outputProps } = useSlider( props, state, trackRef ); return ( <div {...groupProps} className={`slider ${state.orientation}`} > {props.label && ( <div className="label-container"> <label {...labelProps} > {props .label} </label> <output {...outputProps} > {`${ state .getThumbValueLabel( 0 ) } - ${ state .getThumbValueLabel( 1 ) }`} </output> </div> )} <div {...trackProps} ref={trackRef} className={`track ${ state .isDisabled ? 'disabled' : '' }`} > <Thumb index={0} state={state} trackRef={trackRef} /> <Thumb index={1} state={state} trackRef={trackRef} /> </div> </div> ); } <RangeSlider label="Price Range" formatOptions={{ style: 'currency', currency: 'USD' }} maxValue={500} defaultValue={[ 100, 350 ]} step={10} /> Price Range $100.00 - $350.00 ## Usage# * * * The following examples show how to use the `Slider` and `RangeSlider` components created in the above examples. ### Vertical orientation# Sliders are horizontally oriented by default. The `orientation` prop can be set to `"vertical"` to create a vertical slider. This example also uses `aria-label` rather than `label` to create a slider with no visible label. <Slider orientation="vertical" aria-label="Opacity" maxValue={1} step={0.01} /> <Slider orientation="vertical" aria-label="Opacity" maxValue={1} step={0.01} /> <Slider orientation="vertical" aria-label="Opacity" maxValue={1} step={0.01} /> ### Controlled value# The `value` prop paired with the `onChange` event can be used to make a slider controlled. The value must fall between the Slider's minimum and maximum values, which default to 0 and 100 respectively. The `onChange` event receives the new slider value as a parameter, which can be used to update state. function Example() { let [value, setValue] = React.useState(25); return ( <> <Slider label="Cookies to buy" value={value} onChange={setValue} /> <p>Current value: {value}</p> </> ); } function Example() { let [value, setValue] = React.useState(25); return ( <> <Slider label="Cookies to buy" value={value} onChange={setValue} /> <p>Current value: {value}</p> </> ); } function Example() { let [value, setValue] = React.useState(25); return ( <> <Slider label="Cookies to buy" value={value} onChange={setValue} /> <p> Current value: {' '} {value} </p> </> ); } Cookies to buy 25 Current value: 25 Multi thumb sliders specify their values as an array rather than a single number. function Example() { let [value, setValue] = React.useState([25, 75]); return ( <> <RangeSlider label="Range" value={value} onChange={setValue} /> <p>Current value: {value.join(' – ')}</p> </> ); } function Example() { let [value, setValue] = React.useState([25, 75]); return ( <> <RangeSlider label="Range" value={value} onChange={setValue} /> <p>Current value: {value.join(' – ')}</p> </> ); } function Example() { let [value, setValue] = React.useState([ 25, 75 ]); return ( <> <RangeSlider label="Range" value={value} onChange={setValue} /> <p> Current value: {' '} {value.join( ' – ' )} </p> </> ); } Range 25 - 75 Current value: 25 – 75 ### onChangeEnd# The `onChangeEnd` prop can be used to handle when a user stops dragging a slider, whereas the `onChange` prop is called as the user drags. function Example() { let [value, setValue] = React.useState(25); return ( <> <Slider label="Cookies to buy" defaultValue={value} onChangeEnd={setValue} /> <p>Current value: {value}</p> </> ); } function Example() { let [value, setValue] = React.useState(25); return ( <> <Slider label="Cookies to buy" defaultValue={value} onChangeEnd={setValue} /> <p>Current value: {value}</p> </> ); } function Example() { let [value, setValue] = React.useState(25); return ( <> <Slider label="Cookies to buy" defaultValue={value} onChangeEnd={setValue} /> <p> Current value: {' '} {value} </p> </> ); } Cookies to buy 25 Current value: 25 ### Custom value scale# By default, slider values are percentages between 0 and 100. A different scale can be used by setting the `minValue` and `maxValue` props. <Slider label="Cookies to buy" minValue={50} maxValue={150} defaultValue={100} /> <Slider label="Cookies to buy" minValue={50} maxValue={150} defaultValue={100} /> <Slider label="Cookies to buy" minValue={50} maxValue={150} defaultValue={100} /> Cookies to buy 100 ### Value formatting# Values are formatted as a percentage by default, but this can be modified by using the `formatOptions` prop to specify a different format. `formatOptions` is compatible with the option parameter of Intl.NumberFormat and is applied based on the current locale. <Slider label="Currency" formatOptions={{style: 'currency', currency: 'JPY'}} defaultValue={60} /> <Slider label="Currency" formatOptions={{style: 'currency', currency: 'JPY'}} defaultValue={60} /> <Slider label="Currency" formatOptions={{ style: 'currency', currency: 'JPY' }} defaultValue={60} /> Currency ¥60 ### Step values# The `step` prop can be used to snap the value to certain increments. The steps are calculated starting from the minimum. For example, if `minValue={2}`, and `step={3}`, the valid step values would be 2, 5, 8, 11, etc. This example allows increments of 5 between 0 and 100. <Slider label="Amount" formatOptions={{style: 'currency', currency: 'USD'}} minValue={0} maxValue={100} step={5} /> <Slider label="Amount" formatOptions={{style: 'currency', currency: 'USD'}} minValue={0} maxValue={100} step={5} /> <Slider label="Amount" formatOptions={{ style: 'currency', currency: 'USD' }} minValue={0} maxValue={100} step={5} /> Amount $0.00 ### Disabled# A slider can be disabled using the `isDisabled` prop. <Slider label="Cookies to share" defaultValue={25} isDisabled /> <Slider label="Cookies to share" defaultValue={25} isDisabled /> <Slider label="Cookies to share" defaultValue={25} isDisabled /> Cookies to share 25 ### HTML forms# useSliderThumb supports the `name` prop for integration with HTML forms. <Slider label="Opacity" defaultValue={50} name="opacity" /> <Slider label="Opacity" defaultValue={50} name="opacity" /> <Slider label="Opacity" defaultValue={50} name="opacity" /> Opacity 50 ## Internationalization# * * * ### Value formatting# Formatting the value that should be displayed in the value label or `aria-valuetext` is handled by `useSliderState` . The formatting can be controlled using the `formatOptions` prop. If you want to change locales, the` I18nProvider `must be somewhere in the hierarchy above the Slider. This will tell the formatter what locale to use. ### RTL# In right-to-left languages, the slider should be mirrored. The label is right-aligned, the value is left-aligned. Ensure that your CSS accounts for this. --- ## Page: https://react-spectrum.adobe.com/react-aria/useSwitch.html # useSwitch Provides the behavior and accessibility implementation for a switch component. A switch is similar to a checkbox, but represents on/off values as opposed to selection. <table><tbody><tr><th>install</th><td><code>yarn add react-aria</code></td></tr><tr><th>version</th><td>3.41.1</td></tr><tr><th>usage</th><td><code><span>import</span> {useSwitch} <span>from</span> <span>'react-aria'</span></code></td></tr></tbody></table> View ARIA pattern W3C View repository GitHub View package NPM ## API# * * * `useSwitch( props: AriaSwitchProps , state: ToggleState , ref: RefObject <HTMLInputElement | | null> ): SwitchAria ` ## Features# * * * There is no native HTML element with switch styling. `<input type="checkbox">` is the closest semantically, but isn't styled or exposed to assistive technology as a switch. `useSwitch` helps achieve accessible switches that can be styled as needed. * Built with a native HTML `<input>` element, which can be optionally visually hidden to allow custom styling * Full support for browser features like form autofill * Keyboard focus management and cross browser normalization * Labeling support for screen readers * Exposed as a switch to assistive technology via ARIA ## Anatomy# * * * A switch consists of a visual selection indicator and a label. Users may click or touch a switch to toggle the selection state, or use the Tab key to navigate to it and the Space key to toggle it. `useSwitch` returns props to be spread onto its input element: | Name | Type | Description | | --- | --- | --- | | `labelProps` | `LabelHTMLAttributes<HTMLLabelElement>` | Props for the label wrapper element. | | `inputProps` | `InputHTMLAttributes<HTMLInputElement>` | Props for the input element. | | `isSelected` | `boolean` | Whether the switch is selected. | | `isPressed` | `boolean` | Whether the switch is in a pressed state. | | `isDisabled` | `boolean` | Whether the switch is disabled. | | `isReadOnly` | `boolean` | Whether the switch is read only. | Selection state is managed by the `useToggleState` hook in `@react-stately/toggle`. The state object should be passed as an option to `useSwitch`. In most cases, switches should have a visual label. If the switch does not have a visible label, an `aria-label` or `aria-labelledby` prop must be passed instead to identify the element to assistive technology. ## Example# * * * This example uses SVG to build the switch, with a visually hidden native input to represent the switch for accessibility. This is possible using the < `VisuallyHidden` \> utility component from `@react-aria/visually-hidden`. It is still in the DOM and accessible to assistive technology, but invisible. The SVG element is the visual representation, and is hidden from screen readers with `aria-hidden`. For keyboard accessibility, a focus ring is important to indicate which element has keyboard focus. This is implemented with the `useFocusRing` hook from `@react-aria/focus`. When `isFocusVisible` is true, an extra SVG element is rendered to indicate focus. The focus ring is only visible when the user is interacting with a keyboard, not with a mouse or touch. import {useToggleState} from 'react-stately'; import {useFocusRing, useSwitch, VisuallyHidden} from 'react-aria'; function Switch(props) { let state = useToggleState(props); let ref = React.useRef(null); let { inputProps } = useSwitch(props, state, ref); let { isFocusVisible, focusProps } = useFocusRing(); return ( <label style={{ display: 'flex', alignItems: 'center', opacity: props.isDisabled ? 0.4 : 1 }} > <VisuallyHidden> <input {...inputProps} {...focusProps} ref={ref} /> </VisuallyHidden> <svg width={40} height={24} aria-hidden="true" style={{ marginRight: 4 }} > <rect x={4} y={4} width={32} height={16} rx={8} fill={state.isSelected ? 'orange' : 'gray'} /> <circle cx={state.isSelected ? 28 : 12} cy={12} r={5} fill="white" /> {isFocusVisible && ( <rect x={1} y={1} width={38} height={22} rx={11} fill="none" stroke="orange" strokeWidth={2} /> )} </svg> {props.children} </label> ); } <Switch>Low power mode</Switch> import {useToggleState} from 'react-stately'; import { useFocusRing, useSwitch, VisuallyHidden } from 'react-aria'; function Switch(props) { let state = useToggleState(props); let ref = React.useRef(null); let { inputProps } = useSwitch(props, state, ref); let { isFocusVisible, focusProps } = useFocusRing(); return ( <label style={{ display: 'flex', alignItems: 'center', opacity: props.isDisabled ? 0.4 : 1 }} > <VisuallyHidden> <input {...inputProps} {...focusProps} ref={ref} /> </VisuallyHidden> <svg width={40} height={24} aria-hidden="true" style={{ marginRight: 4 }} > <rect x={4} y={4} width={32} height={16} rx={8} fill={state.isSelected ? 'orange' : 'gray'} /> <circle cx={state.isSelected ? 28 : 12} cy={12} r={5} fill="white" /> {isFocusVisible && ( <rect x={1} y={1} width={38} height={22} rx={11} fill="none" stroke="orange" strokeWidth={2} /> )} </svg> {props.children} </label> ); } <Switch>Low power mode</Switch> import {useToggleState} from 'react-stately'; import { useFocusRing, useSwitch, VisuallyHidden } from 'react-aria'; function Switch(props) { let state = useToggleState( props ); let ref = React.useRef( null ); let { inputProps } = useSwitch( props, state, ref ); let { isFocusVisible, focusProps } = useFocusRing(); return ( <label style={{ display: 'flex', alignItems: 'center', opacity: props .isDisabled ? 0.4 : 1 }} > <VisuallyHidden> <input {...inputProps} {...focusProps} ref={ref} /> </VisuallyHidden> <svg width={40} height={24} aria-hidden="true" style={{ marginRight: 4 }} > <rect x={4} y={4} width={32} height={16} rx={8} fill={state .isSelected ? 'orange' : 'gray'} /> <circle cx={state .isSelected ? 28 : 12} cy={12} r={5} fill="white" /> {isFocusVisible && ( <rect x={1} y={1} width={38} height={22} rx={11} fill="none" stroke="orange" strokeWidth={2} /> )} </svg> {props.children} </label> ); } <Switch> Low power mode </Switch> Low power mode ## Usage# * * * The following examples show how to use the `Switch` component created in the above example. ### Default value# Switches are not selected by default. The `defaultSelected` prop can be used to set the default state. <Switch defaultSelected>Wi-Fi</Switch> <Switch defaultSelected>Wi-Fi</Switch> <Switch defaultSelected > Wi-Fi </Switch> Wi-Fi ### Controlled value# The `isSelected` prop can be used to make the selected state controlled. The `onChange` event is fired when the user presses the switch, and receives the new value. function Example() { let [selected, setSelected] = React.useState(false); return ( <> <Switch onChange={setSelected}>Low power mode</Switch> <p>{selected ? 'Low' : 'High'} power mode active.</p> </> ); } function Example() { let [selected, setSelected] = React.useState(false); return ( <> <Switch onChange={setSelected}>Low power mode</Switch> <p>{selected ? 'Low' : 'High'} power mode active.</p> </> ); } function Example() { let [ selected, setSelected ] = React.useState( false ); return ( <> <Switch onChange={setSelected} > Low power mode </Switch> <p> {selected ? 'Low' : 'High'}{' '} power mode active. </p> </> ); } Low power mode High power mode active. ### Disabled# Switches can be disabled using the `isDisabled` prop. <Switch isDisabled>Airplane Mode</Switch> <Switch isDisabled>Airplane Mode</Switch> <Switch isDisabled> Airplane Mode </Switch> Airplane Mode ### Read only# The `isReadOnly` prop makes the selection immutable. Unlike `isDisabled`, the Switch remains focusable. See the MDN docs for more information. <Switch isSelected isReadOnly>Bluetooth</Switch> <Switch isSelected isReadOnly>Bluetooth</Switch> <Switch isSelected isReadOnly > Bluetooth </Switch> Bluetooth ### HTML forms# Switch supports the `name` and `value` props for integration with HTML forms. <Switch name="power" value="low">Low power mode</Switch> <Switch name="power" value="low">Low power mode</Switch> <Switch name="power" value="low" > Low power mode </Switch> Low power mode ## Internationalization# * * * ### RTL# In right-to-left languages, switches should be mirrored. The switch should be placed on the right side of the label. Ensure that your CSS accounts for this. --- ## Page: https://react-spectrum.adobe.com/react-aria/useTextField.html # useTextField Provides the behavior and accessibility implementation for a text field. <table><tbody><tr><th>install</th><td><code>yarn add react-aria</code></td></tr><tr><th>version</th><td>3.41.1</td></tr><tr><th>usage</th><td><code><span>import</span> {useTextField} <span>from</span> <span>'react-aria'</span></code></td></tr></tbody></table> View ARIA pattern W3C View repository GitHub View package NPM ## API# * * * `useTextField<T extends TextFieldIntrinsicElements = DefaultElementType >( (props: AriaTextFieldOptions <T>, , ref: TextFieldRefObject <T> )): TextFieldAria <T>` ## Features# * * * Text fields can be built with <input> or <textarea> and <label> elements, but you must manually ensure that they are semantically connected via ids for accessibility. `useTextField` helps automate this, and handle other accessibility features while allowing for custom styling. * Built with a native `<input>` or `<textarea>` element * Visual and ARIA labeling support * Change, clipboard, composition, selection, and input event support * Support for native HTML constraint validation with customizable UI, custom validation functions, realtime validation, and server-side validation errors * Support for description and error message help text linked to the input via ARIA ## Anatomy# * * * Text fields consist of an input element and a label. `useTextField` automatically manages the relationship between the two elements using the `for` attribute on the `<label>` element and the `aria-labelledby` attribute on the `<input>` element. `useTextField` also supports optional description and error message elements, which can be used to provide more context about the field, and any validation messages. These are linked with the input via the `aria-describedby` attribute. `useTextField` returns props that you should spread onto the appropriate element: | Name | Type | Description | | --- | --- | --- | | `inputProps` | ` TextFieldInputProps < TextFieldIntrinsicElements >` | Props for the input element. | | `labelProps` | `DOMAttributes | LabelHTMLAttributes<HTMLLabelElement>` | Props for the text field's visible label element, if any. | | `descriptionProps` | `DOMAttributes` | Props for the text field's description element, if any. | | `errorMessageProps` | `DOMAttributes` | Props for the text field's error message element, if any. | | `isInvalid` | `boolean` | Whether the input value is invalid. | | `validationErrors` | `string[]` | The current error messages for the input if it is invalid, otherwise an empty array. | | `validationDetails` | `ValidityState` | The native validation details for the input. | If there is no visual label, an `aria-label` or `aria-labelledby` prop must be passed instead to identify the element to screen readers. ## Example# * * * import type {AriaTextFieldProps} from 'react-aria'; import {useTextField} from 'react-aria'; function TextField(props: AriaTextFieldProps) { let { label } = props; let ref = React.useRef(null); let { labelProps, inputProps, descriptionProps, errorMessageProps, isInvalid, validationErrors } = useTextField(props, ref); return ( <div style={{ display: 'flex', flexDirection: 'column', width: 200 }}> <label {...labelProps}>{label}</label> <input {...inputProps} ref={ref} /> {props.description && ( <div {...descriptionProps} style={{ fontSize: 12 }}> {props.description} </div> )} {isInvalid && ( <div {...errorMessageProps} style={{ color: 'red', fontSize: 12 }}> {validationErrors.join(' ')} </div> )} </div> ); } <TextField label="Email" /> import type {AriaTextFieldProps} from 'react-aria'; import {useTextField} from 'react-aria'; function TextField(props: AriaTextFieldProps) { let { label } = props; let ref = React.useRef(null); let { labelProps, inputProps, descriptionProps, errorMessageProps, isInvalid, validationErrors } = useTextField(props, ref); return ( <div style={{ display: 'flex', flexDirection: 'column', width: 200 }} > <label {...labelProps}>{label}</label> <input {...inputProps} ref={ref} /> {props.description && ( <div {...descriptionProps} style={{ fontSize: 12 }}> {props.description} </div> )} {isInvalid && ( <div {...errorMessageProps} style={{ color: 'red', fontSize: 12 }} > {validationErrors.join(' ')} </div> )} </div> ); } <TextField label="Email" /> import type {AriaTextFieldProps} from 'react-aria'; import {useTextField} from 'react-aria'; function TextField( props: AriaTextFieldProps ) { let { label } = props; let ref = React.useRef( null ); let { labelProps, inputProps, descriptionProps, errorMessageProps, isInvalid, validationErrors } = useTextField( props, ref ); return ( <div style={{ display: 'flex', flexDirection: 'column', width: 200 }} > <label {...labelProps} > {label} </label> <input {...inputProps} ref={ref} /> {props .description && ( <div {...descriptionProps} style={{ fontSize: 12 }} > {props .description} </div> )} {isInvalid && ( <div {...errorMessageProps} style={{ color: 'red', fontSize: 12 }} > {validationErrors .join(' ')} </div> )} </div> ); } <TextField label="Email" /> Email ### Text area# `useTextField` also supports multi-line text entry with the `<textarea>` element via the `inputElementType` prop. import type {AriaTextFieldProps} from 'react-aria'; import {useTextField} from 'react-aria'; function TextArea(props: AriaTextFieldProps<HTMLTextAreaElement>) { let { label } = props; let ref = React.useRef(null); let { labelProps, inputProps } = useTextField({ ...props, inputElementType: 'textarea' }, ref); return ( <div style={{ display: 'flex', flexDirection: 'column', width: 200 }}> <label {...labelProps}>{label}</label> <textarea {...inputProps} ref={ref} /> </div> ); } <TextArea label="Description" /> import type {AriaTextFieldProps} from 'react-aria'; import {useTextField} from 'react-aria'; function TextArea( props: AriaTextFieldProps<HTMLTextAreaElement> ) { let { label } = props; let ref = React.useRef(null); let { labelProps, inputProps } = useTextField({ ...props, inputElementType: 'textarea' }, ref); return ( <div style={{ display: 'flex', flexDirection: 'column', width: 200 }} > <label {...labelProps}>{label}</label> <textarea {...inputProps} ref={ref} /> </div> ); } <TextArea label="Description" /> import type {AriaTextFieldProps} from 'react-aria'; import {useTextField} from 'react-aria'; function TextArea( props: AriaTextFieldProps< HTMLTextAreaElement > ) { let { label } = props; let ref = React.useRef( null ); let { labelProps, inputProps } = useTextField({ ...props, inputElementType: 'textarea' }, ref); return ( <div style={{ display: 'flex', flexDirection: 'column', width: 200 }} > <label {...labelProps} > {label} </label> <textarea {...inputProps} ref={ref} /> </div> ); } <TextArea label="Description" /> Description ## Usage# * * * The following examples show how to use the `TextField` component created in the above example. ### Default value# A TextField's `value` is empty by default, but an initial, uncontrolled, value can be provided using the `defaultValue` prop. <TextField label="Email" defaultValue="me@email.com" /> <TextField label="Email" defaultValue="me@email.com" /> <TextField label="Email" defaultValue="me@email.com" /> Email ### Controlled value# The `value` prop can be used to make the value controlled. The `onChange` event is fired when the user edits the text, and receives the new value. function Example() { let [text, setText] = React.useState(''); return ( <> <TextField label="Your text" onChange={setText} /> <p>Mirrored text: {text}</p> </> ); } function Example() { let [text, setText] = React.useState(''); return ( <> <TextField label="Your text" onChange={setText} /> <p>Mirrored text: {text}</p> </> ); } function Example() { let [text, setText] = React.useState(''); return ( <> <TextField label="Your text" onChange={setText} /> <p> Mirrored text: {' '} {text} </p> </> ); } Your text Mirrored text: ### Description# The `description` prop can be used to associate additional help text with a text field. <TextField label="Email" description="Enter an email for us to contact you about your order." /> <TextField label="Email" description="Enter an email for us to contact you about your order." /> <TextField label="Email" description="Enter an email for us to contact you about your order." /> Email Enter an email for us to contact you about your order. ### Validation# useTextField supports HTML constraint validation props such as `isRequired`, `type="email"`, `minLength`, and `pattern`, as well as custom validation functions, realtime validation, and server-side validation. It can also be integrated with other form libraries. See the Forms guide to learn more. When a TextField has the `validationBehavior="native"` prop, validation errors block form submission. To display validation errors, use the `validationErrors` and `errorMessageProps` returned by `useTextField`. This allows you to render error messages from all of the above sources with consistent custom styles. <form> <TextField label="Email" name="email" type="email" isRequired validationBehavior="native" /> <input type="submit" style={{marginTop: 8}} /> </form> <form> <TextField label="Email" name="email" type="email" isRequired validationBehavior="native" /> <input type="submit" style={{marginTop: 8}} /> </form> <form> <TextField label="Email" name="email" type="email" isRequired validationBehavior="native" /> <input type="submit" style={{ marginTop: 8 }} /> </form> Email ### Disabled# A TextField can be disabled using the `isDisabled` prop. <TextField label="Email" isDisabled /> <TextField label="Email" isDisabled /> <TextField label="Email" isDisabled /> Email ### Read only# The `isReadOnly` boolean prop makes the TextField's text content immutable. Unlike `isDisabled`, the TextField remains focusable and the contents can still be copied. See the MDN docs for more information. <TextField label="Email" defaultValue="abc@adobe.com" isReadOnly /> <TextField label="Email" defaultValue="abc@adobe.com" isReadOnly /> <TextField label="Email" defaultValue="abc@adobe.com" isReadOnly /> Email ### Required# A TextField can be marked as required using the `isRequired` prop. This is exposed to assistive technologies by React Aria. It's your responsibility to add additional visual styling if needed. <TextField label="Email" isRequired /> <TextField label="Email" isRequired /> <TextField label="Email" isRequired /> Email ### HTML forms# TextField supports the `name` prop for integration with HTML forms. In addition, attributes such as `type`, `pattern`, `inputMode`, and others are passed through to the underlying `<input>` element. <TextField label="Email" name="email" type="email" /> <TextField label="Email" name="email" type="email" /> <TextField label="Email" name="email" type="email" /> Email ## Internationalization# * * * ### RTL# In right-to-left languages, text fields should be mirrored. The label should be right aligned, along with the text in the text field. Ensure that your CSS accounts for this. --- ## Page: https://react-spectrum.adobe.com/react-aria/useBreadcrumbs.html # useBreadcrumbs Provides the behavior and accessibility implementation for a breadcrumbs component. Breadcrumbs display a hierarchy of links to the current page or resource in an application. <table><tbody><tr><th>install</th><td><code>yarn add react-aria</code></td></tr><tr><th>version</th><td>3.41.1</td></tr><tr><th>usage</th><td><code><span>import</span> {useBreadcrumbs, useBreadcrumbItem} <span>from</span> <span>'react-aria'</span></code></td></tr></tbody></table> View ARIA pattern W3C View repository GitHub View package NPM ## API# * * * `useBreadcrumbs( (props: AriaBreadcrumbsProps )): BreadcrumbsAria ``useBreadcrumbItem( (props: AriaBreadcrumbItemProps , , ref: RefObject <FocusableElement | | null> )): BreadcrumbItemAria ` ## Features# * * * Breadcrumbs provide a list of links to parent pages of the current page in hierarchical order. `useBreadcrumbs` and `useBreadcrumbItem` help implement these in an accessible way. * Support for mouse, touch, and keyboard interactions on breadcrumbs * Support for navigation links via `<a>` elements or custom element types via ARIA * Localized ARIA labeling support for landmark navigation region * Support for disabled breadcrumbs ## Anatomy# * * * Breadcrumbs consist of a navigation landmark element and a list of links, typically with a visual separator icon between each item. The last link represents the current page in the hierarchy, with the previous links representing the parent pages of the current page. Each of these parent links can be clicked, tapped, or triggered via the Enter key to navigate to that page. `useBreadcrumbs` returns props to be spread onto the navigation element: | Name | Type | Description | | --- | --- | --- | | `navProps` | `DOMAttributes` | Props for the breadcrumbs navigation element. | `useBreadcrumbItem` returns props to spread onto the individual breadcrumb links: | Name | Type | Description | | --- | --- | --- | | `itemProps` | `DOMAttributes` | Props for the breadcrumb item link element. | ## Example# * * * This example displays a basic list of breadcrumbs using an HTML <nav> element, and a <ol> for the list of links. Each link is a span because we are handling the interactions locally via `onPress`. `useBreadcrumbItem` automatically handles exposing these spans as links to assistive technology. The chevrons between each link are rendered using a span with `aria-hidden="true"` so that screen readers do not pick them up. You could also render them similarly using SVG icons, CSS `:after`, or other techniques. The last link is non-interactive since it represents the current page. This is passed to the last breadcrumb item by cloning the element and adding the `isCurrent` prop. import {useBreadcrumbItem, useBreadcrumbs} from 'react-aria'; function Breadcrumbs(props) { let { navProps } = useBreadcrumbs(props); let childCount = React.Children.count(props.children); return ( <nav {...navProps}> <ol style={{ display: 'flex', listStyle: 'none', margin: 0, padding: 0 }}> {React.Children.map(props.children, (child, i) => React.cloneElement(child, { isCurrent: i === childCount - 1 }))} </ol> </nav> ); } function BreadcrumbItem(props) { let ref = React.useRef(null); let { itemProps } = useBreadcrumbItem({ ...props, elementType: 'span' }, ref); return ( <li> <span {...itemProps} ref={ref} style={{ color: props.isDisabled ? 'var(--gray)' : 'var(--blue)', textDecoration: props.isCurrent || props.isDisabled ? 'none' : 'underline', fontWeight: props.isCurrent ? 'bold' : null, cursor: props.isCurrent || props.isDisabled ? 'default' : 'pointer' }} > {props.children} </span> {!props.isCurrent && <span aria-hidden="true" style={{ padding: '0 5px' }}>{'›'}</span>} </li> ); } <Breadcrumbs> <BreadcrumbItem onPress={() => alert('Pressed Folder 1')}> Folder 1 </BreadcrumbItem> <BreadcrumbItem onPress={() => alert('Pressed Folder 2')}> Folder 2 </BreadcrumbItem> <BreadcrumbItem>Folder 3</BreadcrumbItem> </Breadcrumbs> import { useBreadcrumbItem, useBreadcrumbs } from 'react-aria'; function Breadcrumbs(props) { let { navProps } = useBreadcrumbs(props); let childCount = React.Children.count(props.children); return ( <nav {...navProps}> <ol style={{ display: 'flex', listStyle: 'none', margin: 0, padding: 0 }} > {React.Children.map(props.children, (child, i) => React.cloneElement(child, { isCurrent: i === childCount - 1 }))} </ol> </nav> ); } function BreadcrumbItem(props) { let ref = React.useRef(null); let { itemProps } = useBreadcrumbItem({ ...props, elementType: 'span' }, ref); return ( <li> <span {...itemProps} ref={ref} style={{ color: props.isDisabled ? 'var(--gray)' : 'var(--blue)', textDecoration: props.isCurrent || props.isDisabled ? 'none' : 'underline', fontWeight: props.isCurrent ? 'bold' : null, cursor: props.isCurrent || props.isDisabled ? 'default' : 'pointer' }} > {props.children} </span> {!props.isCurrent && ( <span aria-hidden="true" style={{ padding: '0 5px' }} > {'›'} </span> )} </li> ); } <Breadcrumbs> <BreadcrumbItem onPress={() => alert('Pressed Folder 1')} > Folder 1 </BreadcrumbItem> <BreadcrumbItem onPress={() => alert('Pressed Folder 2')} > Folder 2 </BreadcrumbItem> <BreadcrumbItem>Folder 3</BreadcrumbItem> </Breadcrumbs> import { useBreadcrumbItem, useBreadcrumbs } from 'react-aria'; function Breadcrumbs( props ) { let { navProps } = useBreadcrumbs( props ); let childCount = React .Children.count( props.children ); return ( <nav {...navProps}> <ol style={{ display: 'flex', listStyle: 'none', margin: 0, padding: 0 }} > {React.Children .map( props .children, (child, i) => React .cloneElement( child, { isCurrent: i === childCount - 1 } ) )} </ol> </nav> ); } function BreadcrumbItem( props ) { let ref = React.useRef( null ); let { itemProps } = useBreadcrumbItem({ ...props, elementType: 'span' }, ref); return ( <li> <span {...itemProps} ref={ref} style={{ color: props .isDisabled ? 'var(--gray)' : 'var(--blue)', textDecoration: props .isCurrent || props .isDisabled ? 'none' : 'underline', fontWeight: props .isCurrent ? 'bold' : null, cursor: props .isCurrent || props .isDisabled ? 'default' : 'pointer' }} > {props.children} </span> {!props .isCurrent && ( <span aria-hidden="true" style={{ padding: '0 5px' }} > {'›'} </span> )} </li> ); } <Breadcrumbs> <BreadcrumbItem onPress={() => alert( 'Pressed Folder 1' )} > Folder 1 </BreadcrumbItem> <BreadcrumbItem onPress={() => alert( 'Pressed Folder 2' )} > Folder 2 </BreadcrumbItem> <BreadcrumbItem> Folder 3 </BreadcrumbItem> </Breadcrumbs> 1. Folder 1› 2. Folder 2› 3. Folder 3 ## Navigation links# * * * To render breadcrumbs that navigate to other pages rather than handle events via `onPress`, use an `<a>` element for each BreadcrumbItem. This is the default `elementType`, so the option can be omitted from `useBreadcrumbItem`. function BreadcrumbItem(props) { let ref = React.useRef(null); let { itemProps } = useBreadcrumbItem(props, ref); return ( <li> <a {...itemProps} ref={ref} href={props.href} style={{ color: props.isDisabled ? 'var(--gray)' : 'var(--blue)', textDecoration: props.isCurrent || props.isDisabled ? 'none' : 'underline', fontWeight: props.isCurrent ? 'bold' : null, cursor: props.isCurrent || props.isDisabled ? 'default' : 'pointer' }} > {props.children} </a> {!props.isCurrent && <span aria-hidden="true" style={{ padding: '0 5px' }}>{'›'}</span>} </li> ); } <Breadcrumbs> <BreadcrumbItem href="/">Home</BreadcrumbItem> <BreadcrumbItem href="/react-aria/">React Aria</BreadcrumbItem> <BreadcrumbItem>useBreadcrumbs</BreadcrumbItem> </Breadcrumbs> function BreadcrumbItem(props) { let ref = React.useRef(null); let { itemProps } = useBreadcrumbItem(props, ref); return ( <li> <a {...itemProps} ref={ref} href={props.href} style={{ color: props.isDisabled ? 'var(--gray)' : 'var(--blue)', textDecoration: props.isCurrent || props.isDisabled ? 'none' : 'underline', fontWeight: props.isCurrent ? 'bold' : null, cursor: props.isCurrent || props.isDisabled ? 'default' : 'pointer' }} > {props.children} </a> {!props.isCurrent && ( <span aria-hidden="true" style={{ padding: '0 5px' }} > {'›'} </span> )} </li> ); } <Breadcrumbs> <BreadcrumbItem href="/">Home</BreadcrumbItem> <BreadcrumbItem href="/react-aria/"> React Aria </BreadcrumbItem> <BreadcrumbItem>useBreadcrumbs</BreadcrumbItem> </Breadcrumbs> function BreadcrumbItem( props ) { let ref = React.useRef( null ); let { itemProps } = useBreadcrumbItem( props, ref ); return ( <li> <a {...itemProps} ref={ref} href={props.href} style={{ color: props .isDisabled ? 'var(--gray)' : 'var(--blue)', textDecoration: props .isCurrent || props .isDisabled ? 'none' : 'underline', fontWeight: props .isCurrent ? 'bold' : null, cursor: props .isCurrent || props .isDisabled ? 'default' : 'pointer' }} > {props.children} </a> {!props .isCurrent && ( <span aria-hidden="true" style={{ padding: '0 5px' }} > {'›'} </span> )} </li> ); } <Breadcrumbs> <BreadcrumbItem href="/"> Home </BreadcrumbItem> <BreadcrumbItem href="/react-aria/"> React Aria </BreadcrumbItem> <BreadcrumbItem> useBreadcrumbs </BreadcrumbItem> </Breadcrumbs> 1. Home› 2. React Aria› 3. useBreadcrumbs ## Usage# * * * The following examples show how to use the `Breadcrumbs` component created in the above examples. ### Disabled# Breadcrumbs can be disabled using the `isDisabled` prop, passed to each disabled BreadcrumbItem. This indicates that navigation is not currently available. When a breadcrumb is disabled, `onPress` will not be triggered, navigation will not occur, and links will be marked as `aria-disabled` for assistive technologies. <Breadcrumbs> <BreadcrumbItem href="/" isDisabled>Home</BreadcrumbItem> <BreadcrumbItem href="/react-aria/">React Aria</BreadcrumbItem> <BreadcrumbItem>useBreadcrumbs</BreadcrumbItem> </Breadcrumbs> <Breadcrumbs> <BreadcrumbItem href="/" isDisabled> Home </BreadcrumbItem> <BreadcrumbItem href="/react-aria/"> React Aria </BreadcrumbItem> <BreadcrumbItem>useBreadcrumbs</BreadcrumbItem> </Breadcrumbs> <Breadcrumbs> <BreadcrumbItem href="/" isDisabled > Home </BreadcrumbItem> <BreadcrumbItem href="/react-aria/"> React Aria </BreadcrumbItem> <BreadcrumbItem> useBreadcrumbs </BreadcrumbItem> </Breadcrumbs> 1. Home› 2. React Aria› 3. useBreadcrumbs --- ## Page: https://react-spectrum.adobe.com/react-aria/useDisclosure.html # useDisclosure Provides the behavior and accessibility implementation for a disclosure component. <table><tbody><tr><th>install</th><td><code>yarn add react-aria</code></td></tr><tr><th>version</th><td>3.41.1</td></tr><tr><th>usage</th><td><code><span>import</span> {useDisclosure} <span>from</span> <span>'react-aria'</span></code></td></tr></tbody></table> View ARIA pattern W3C View repository GitHub View package NPM ## API# * * * `useDisclosure( props: AriaDisclosureProps , state: DisclosureState , ref: RefObject<Element | | null> ): DisclosureAria ` ## Features# * * * A disclosure is a collapsible section of content. It is composed of a trigger button and a panel that contains the content. `useDisclosure` can be used to implement these in an accessible way. * Support for mouse, touch, and keyboard interactions to open and close the disclosure * Support for disabled disclosures * Follows the disclosure ARIA pattern, semantically linking the trigger button and panel * Uses hidden="until-found" in supported browsers, enabling find-in-page search support and improved search engine visibility for collapsed content ## Anatomy# * * * A disclosure consists of a trigger button and a panel. Clicking on or pressing Enter or Space while the trigger button is focused toggles the visibility of the panel. `useDisclosure` returns props to spread onto the trigger button and panel. | Name | Type | Description | | --- | --- | --- | | `buttonProps` | ` AriaButtonProps ` | Props for the disclosure button. | | `panelProps` | `HTMLAttributes<HTMLElement>` | Props for the disclosure panel. | State is managed by the `useDisclosureState` hook in `@react-stately/disclosure`. The state object should be passed as an option to `useDisclosure`. ## Example# * * * This example displays a basic disclosure with a button that toggles the visibility of the panel. import {useButton, useDisclosure} from 'react-aria'; import {useDisclosureState} from 'react-stately'; import {mergeProps, useFocusRing} from 'react-aria'; function Disclosure(props) { let state = useDisclosureState(props); let panelRef = React.useRef<HTMLDivElement | null>(null); let triggerRef = React.useRef<HTMLButtonElement | null>(null); let { buttonProps: triggerProps, panelProps } = useDisclosure( props, state, panelRef ); let { buttonProps } = useButton(triggerProps, triggerRef); let { isFocusVisible, focusProps } = useFocusRing(); return ( <div className="disclosure"> <h3> <button className="trigger" ref={triggerRef} {...mergeProps(buttonProps, focusProps)} style={{ outline: isFocusVisible ? '2px solid dodgerblue' : 'none' }} > <svg viewBox="0 0 24 24"> <path d="m8.25 4.5 7.5 7.5-7.5 7.5" /> </svg> {props.title} </button> </h3> <div className="panel" ref={panelRef} {...panelProps}> <p> {props.children} </p> </div> </div> ); } <Disclosure title="System Requirements"> Details about system requirements here. </Disclosure> import {useButton, useDisclosure} from 'react-aria'; import {useDisclosureState} from 'react-stately'; import {mergeProps, useFocusRing} from 'react-aria'; function Disclosure(props) { let state = useDisclosureState(props); let panelRef = React.useRef<HTMLDivElement | null>(null); let triggerRef = React.useRef<HTMLButtonElement | null>( null ); let { buttonProps: triggerProps, panelProps } = useDisclosure(props, state, panelRef); let { buttonProps } = useButton(triggerProps, triggerRef); let { isFocusVisible, focusProps } = useFocusRing(); return ( <div className="disclosure"> <h3> <button className="trigger" ref={triggerRef} {...mergeProps(buttonProps, focusProps)} style={{ outline: isFocusVisible ? '2px solid dodgerblue' : 'none' }} > <svg viewBox="0 0 24 24"> <path d="m8.25 4.5 7.5 7.5-7.5 7.5" /> </svg> {props.title} </button> </h3> <div className="panel" ref={panelRef} {...panelProps}> <p> {props.children} </p> </div> </div> ); } <Disclosure title="System Requirements"> Details about system requirements here. </Disclosure> import { useButton, useDisclosure } from 'react-aria'; import {useDisclosureState} from 'react-stately'; import { mergeProps, useFocusRing } from 'react-aria'; function Disclosure( props ) { let state = useDisclosureState( props ); let panelRef = React .useRef< | HTMLDivElement | null >(null); let triggerRef = React .useRef< | HTMLButtonElement | null >(null); let { buttonProps: triggerProps, panelProps } = useDisclosure( props, state, panelRef ); let { buttonProps } = useButton( triggerProps, triggerRef ); let { isFocusVisible, focusProps } = useFocusRing(); return ( <div className="disclosure"> <h3> <button className="trigger" ref={triggerRef} {...mergeProps( buttonProps, focusProps )} style={{ outline: isFocusVisible ? '2px solid dodgerblue' : 'none' }} > <svg viewBox="0 0 24 24"> <path d="m8.25 4.5 7.5 7.5-7.5 7.5" /> </svg> {props.title} </button> </h3> <div className="panel" ref={panelRef} {...panelProps} > <p> {props .children} </p> </div> </div> ); } <Disclosure title="System Requirements"> Details about system requirements here. </Disclosure> ### System Requirements Details about system requirements here. Show CSS @import "@react-aria/example-theme"; .disclosure { .trigger { background: none; border: none; box-shadow: none; font-weight: bold; font-size: 16px; display: flex; align-items: center; gap: 8px; color: var(--text-color); svg { rotate: 0deg; transition: rotate 200ms; width: 12px; height: 12px; fill: none; stroke: currentColor; stroke-width: 3px; } &[aria-expanded="true"] svg { rotate: 90deg; } &:disabled { color: var(--gray-300); } } } .panel { margin-left: 32px; } @import "@react-aria/example-theme"; .disclosure { .trigger { background: none; border: none; box-shadow: none; font-weight: bold; font-size: 16px; display: flex; align-items: center; gap: 8px; color: var(--text-color); svg { rotate: 0deg; transition: rotate 200ms; width: 12px; height: 12px; fill: none; stroke: currentColor; stroke-width: 3px; } &[aria-expanded="true"] svg { rotate: 90deg; } &:disabled { color: var(--gray-300); } } } .panel { margin-left: 32px; } @import "@react-aria/example-theme"; .disclosure { .trigger { background: none; border: none; box-shadow: none; font-weight: bold; font-size: 16px; display: flex; align-items: center; gap: 8px; color: var(--text-color); svg { rotate: 0deg; transition: rotate 200ms; width: 12px; height: 12px; fill: none; stroke: currentColor; stroke-width: 3px; } &[aria-expanded="true"] svg { rotate: 90deg; } &:disabled { color: var(--gray-300); } } } .panel { margin-left: 32px; } ## Usage# * * * The following examples show how to use the `Disclosure` component created in the above example. ### Default expansion# Whether or not the disclosure is expanded or not by default can be set with the `defaultExpanded` prop. <Disclosure title="System Requirements" defaultExpanded> Details about system requirements here. </Disclosure> <Disclosure title="System Requirements" defaultExpanded> Details about system requirements here. </Disclosure> <Disclosure title="System Requirements" defaultExpanded > Details about system requirements here. </Disclosure> ### System Requirements Details about system requirements here. ### Controlled expansion# Expansion can be controlled using the `isExpanded` prop, paired with the `onExpandedChange` event. The `onExpandedChange` event is fired when the user presses the trigger button. function ControlledDisclosure(props) { let [isExpanded, setExpanded] = React.useState(false); return ( <Disclosure title="System Requirements" isExpanded={isExpanded} onExpandedChange={setExpanded} > Details about system requirements here. </Disclosure> ); } function ControlledDisclosure(props) { let [isExpanded, setExpanded] = React.useState(false); return ( <Disclosure title="System Requirements" isExpanded={isExpanded} onExpandedChange={setExpanded} > Details about system requirements here. </Disclosure> ); } function ControlledDisclosure( props ) { let [ isExpanded, setExpanded ] = React.useState( false ); return ( <Disclosure title="System Requirements" isExpanded={isExpanded} onExpandedChange={setExpanded} > Details about system requirements here. </Disclosure> ); } ### System Requirements Details about system requirements here. ### Disabled# A disclosure can be disabled with the `isDisabled` prop. This will disable the trigger button and prevent the panel from being opened or closed. <Disclosure title="System Requirements" isDisabled> Details about system requirements here. </Disclosure> <Disclosure title="System Requirements" isDisabled> Details about system requirements here. </Disclosure> <Disclosure title="System Requirements" isDisabled > Details about system requirements here. </Disclosure> ### System Requirements Details about system requirements here. ## Disclosure Group# * * * A disclosure group (i.e. accordion) is a set of disclosures where only one disclosure can be expanded at a time. The following example shows how to create a `DisclosureGroup` component with the `useDisclosureGroupState` hook. We'll also create a `DisclosureItem` component that uses the `DisclosureGroupState` context for managing its state. import {useId} from 'react-aria'; import {useDisclosureGroupState} from 'react-stately'; const DisclosureGroupStateContext = React.createContext(null); function DisclosureGroup(props) { let state = useDisclosureGroupState(props); return ( <div className="group"> <DisclosureGroupStateContext.Provider value={state}> {props.children} </DisclosureGroupStateContext.Provider> </div> ); } function DisclosureItem(props) { let defaultId = useId(); let id = props.id || defaultId; let groupState = React.useContext(DisclosureGroupStateContext); let isExpanded = groupState ? groupState.expandedKeys.has(id) : props.isExpanded; let state = useDisclosureState({ ...props, isExpanded, onExpandedChange(isExpanded) { if (groupState) { groupState.toggleKey(id); } props.onExpandedChange?.(isExpanded); } }); let panelRef = React.useRef<HTMLDivElement | null>(null); let triggerRef = React.useRef<HTMLButtonElement | null>(null); let isDisabled = props.isDisabled || groupState?.isDisabled || false; let { buttonProps: triggerProps, panelProps } = useDisclosure( { ...props, isExpanded, isDisabled }, state, panelRef ); let { buttonProps } = useButton(triggerProps, triggerRef); let { isFocusVisible, focusProps } = useFocusRing(); return ( <div className="disclosure"> <h3> <button className="trigger" ref={triggerRef} {...mergeProps(buttonProps, focusProps)} style={{ outline: isFocusVisible ? '2px solid dodgerblue' : 'none' }} > <svg viewBox="0 0 24 24"> <path d="m8.25 4.5 7.5 7.5-7.5 7.5" /> </svg> {props.title} </button> </h3> <div className="panel" ref={panelRef} {...panelProps}> <p> {props.children} </p> </div> </div> ); } import {useId} from 'react-aria'; import {useDisclosureGroupState} from 'react-stately'; const DisclosureGroupStateContext = React.createContext( null ); function DisclosureGroup(props) { let state = useDisclosureGroupState(props); return ( <div className="group"> <DisclosureGroupStateContext.Provider value={state}> {props.children} </DisclosureGroupStateContext.Provider> </div> ); } function DisclosureItem(props) { let defaultId = useId(); let id = props.id || defaultId; let groupState = React.useContext( DisclosureGroupStateContext ); let isExpanded = groupState ? groupState.expandedKeys.has(id) : props.isExpanded; let state = useDisclosureState({ ...props, isExpanded, onExpandedChange(isExpanded) { if (groupState) { groupState.toggleKey(id); } props.onExpandedChange?.(isExpanded); } }); let panelRef = React.useRef<HTMLDivElement | null>(null); let triggerRef = React.useRef<HTMLButtonElement | null>( null ); let isDisabled = props.isDisabled || groupState?.isDisabled || false; let { buttonProps: triggerProps, panelProps } = useDisclosure( { ...props, isExpanded, isDisabled }, state, panelRef ); let { buttonProps } = useButton(triggerProps, triggerRef); let { isFocusVisible, focusProps } = useFocusRing(); return ( <div className="disclosure"> <h3> <button className="trigger" ref={triggerRef} {...mergeProps(buttonProps, focusProps)} style={{ outline: isFocusVisible ? '2px solid dodgerblue' : 'none' }} > <svg viewBox="0 0 24 24"> <path d="m8.25 4.5 7.5 7.5-7.5 7.5" /> </svg> {props.title} </button> </h3> <div className="panel" ref={panelRef} {...panelProps}> <p> {props.children} </p> </div> </div> ); } import {useId} from 'react-aria'; import {useDisclosureGroupState} from 'react-stately'; const DisclosureGroupStateContext = React.createContext( null ); function DisclosureGroup( props ) { let state = useDisclosureGroupState( props ); return ( <div className="group"> <DisclosureGroupStateContext.Provider value={state} > {props.children} </DisclosureGroupStateContext.Provider> </div> ); } function DisclosureItem( props ) { let defaultId = useId(); let id = props.id || defaultId; let groupState = React .useContext( DisclosureGroupStateContext ); let isExpanded = groupState ? groupState .expandedKeys .has(id) : props.isExpanded; let state = useDisclosureState({ ...props, isExpanded, onExpandedChange( isExpanded ) { if (groupState) { groupState .toggleKey( id ); } props .onExpandedChange?.( isExpanded ); } }); let panelRef = React .useRef< | HTMLDivElement | null >(null); let triggerRef = React .useRef< | HTMLButtonElement | null >(null); let isDisabled = props.isDisabled || groupState ?.isDisabled || false; let { buttonProps: triggerProps, panelProps } = useDisclosure( { ...props, isExpanded, isDisabled }, state, panelRef ); let { buttonProps } = useButton( triggerProps, triggerRef ); let { isFocusVisible, focusProps } = useFocusRing(); return ( <div className="disclosure"> <h3> <button className="trigger" ref={triggerRef} {...mergeProps( buttonProps, focusProps )} style={{ outline: isFocusVisible ? '2px solid dodgerblue' : 'none' }} > <svg viewBox="0 0 24 24"> <path d="m8.25 4.5 7.5 7.5-7.5 7.5" /> </svg> {props.title} </button> </h3> <div className="panel" ref={panelRef} {...panelProps} > <p> {props .children} </p> </div> </div> ); } ### Usage# The following examples show how to use the `DisclosureGroup` component created in the above example. <DisclosureGroup> <DisclosureItem title="Personal Information"> Personal information form here. </DisclosureItem> <DisclosureItem title="Billing Address"> Billing address form here. </DisclosureItem> </DisclosureGroup> <DisclosureGroup> <DisclosureItem title="Personal Information"> Personal information form here. </DisclosureItem> <DisclosureItem title="Billing Address"> Billing address form here. </DisclosureItem> </DisclosureGroup> <DisclosureGroup> <DisclosureItem title="Personal Information"> Personal information form here. </DisclosureItem> <DisclosureItem title="Billing Address"> Billing address form here. </DisclosureItem> </DisclosureGroup> ### Personal Information Personal information form here. ### Billing Address Billing address form here. #### Default expansion# Which disclosure is expanded by default can be set with the `defaultExpandedKeys` prop. <DisclosureGroup defaultExpandedKeys={['billing']}> <DisclosureItem id="personal" title="Personal Information"> Personal information form here. </DisclosureItem> <DisclosureItem id="billing" title="Billing Address"> Billing address form here. </DisclosureItem> </DisclosureGroup> <DisclosureGroup defaultExpandedKeys={['billing']}> <DisclosureItem id="personal" title="Personal Information" > Personal information form here. </DisclosureItem> <DisclosureItem id="billing" title="Billing Address"> Billing address form here. </DisclosureItem> </DisclosureGroup> <DisclosureGroup defaultExpandedKeys={[ 'billing' ]} > <DisclosureItem id="personal" title="Personal Information" > Personal information form here. </DisclosureItem> <DisclosureItem id="billing" title="Billing Address" > Billing address form here. </DisclosureItem> </DisclosureGroup> ### Personal Information Personal information form here. ### Billing Address Billing address form here. #### Controlled expansion# Expansion can be controlled using the `expandedKeys` prop, paired with the `onExpandedChange` event. The `onExpandedChange` event is fired when one of the disclosures is expanded or collapsed. function ControlledDisclosureGroup(props) { let [expandedKeys, setExpandedKeys] = React.useState(['personal']); return ( <DisclosureGroup expandedKeys={expandedKeys} onExpandedChange={setExpandedKeys} > <DisclosureItem id="personal" title="Personal Information"> Personal information form here. </DisclosureItem> <DisclosureItem id="billing" title="Billing Address"> Billing address form here. </DisclosureItem> </DisclosureGroup> ); } function ControlledDisclosureGroup(props) { let [expandedKeys, setExpandedKeys] = React.useState([ 'personal' ]); return ( <DisclosureGroup expandedKeys={expandedKeys} onExpandedChange={setExpandedKeys} > <DisclosureItem id="personal" title="Personal Information" > Personal information form here. </DisclosureItem> <DisclosureItem id="billing" title="Billing Address"> Billing address form here. </DisclosureItem> </DisclosureGroup> ); } function ControlledDisclosureGroup( props ) { let [ expandedKeys, setExpandedKeys ] = React.useState([ 'personal' ]); return ( <DisclosureGroup expandedKeys={expandedKeys} onExpandedChange={setExpandedKeys} > <DisclosureItem id="personal" title="Personal Information" > Personal information form here. </DisclosureItem> <DisclosureItem id="billing" title="Billing Address" > Billing address form here. </DisclosureItem> </DisclosureGroup> ); } ### Personal Information Personal information form here. ### Billing Address Billing address form here. #### Multiple expanded# Multiple disclosures can be expanded at the same time by setting the `allowsMultipleExpanded` prop to `true`. <DisclosureGroup allowsMultipleExpanded> <DisclosureItem title="Personal Information"> Personal information form here. </DisclosureItem> <DisclosureItem title="Billing Address"> Billing address form here. </DisclosureItem> </DisclosureGroup> <DisclosureGroup allowsMultipleExpanded> <DisclosureItem title="Personal Information"> Personal information form here. </DisclosureItem> <DisclosureItem title="Billing Address"> Billing address form here. </DisclosureItem> </DisclosureGroup> <DisclosureGroup allowsMultipleExpanded > <DisclosureItem title="Personal Information"> Personal information form here. </DisclosureItem> <DisclosureItem title="Billing Address"> Billing address form here. </DisclosureItem> </DisclosureGroup> ### Personal Information Personal information form here. ### Billing Address Billing address form here. #### Disabled# An entire disclosure group can be disabled with the `isDisabled` prop. This will disable all trigger buttons and prevent the panels from being opened or closed. <DisclosureGroup isDisabled> <DisclosureItem title="Personal Information"> Personal information form here. </DisclosureItem> <DisclosureItem title="Billing Address"> Billing address form here. </DisclosureItem> </DisclosureGroup> <DisclosureGroup isDisabled> <DisclosureItem title="Personal Information"> Personal information form here. </DisclosureItem> <DisclosureItem title="Billing Address"> Billing address form here. </DisclosureItem> </DisclosureGroup> <DisclosureGroup isDisabled > <DisclosureItem title="Personal Information"> Personal information form here. </DisclosureItem> <DisclosureItem title="Billing Address"> Billing address form here. </DisclosureItem> </DisclosureGroup> ### Personal Information Personal information form here. ### Billing Address Billing address form here. --- ## Page: https://react-spectrum.adobe.com/react-aria/useLink.html # useLink Provides the behavior and accessibility implementation for a link component. A link allows a user to navigate to another page or resource within a web page or application. <table><tbody><tr><th>install</th><td><code>yarn add react-aria</code></td></tr><tr><th>version</th><td>3.41.1</td></tr><tr><th>usage</th><td><code><span>import</span> {useLink} <span>from</span> <span>'react-aria'</span></code></td></tr></tbody></table> View ARIA pattern W3C View repository GitHub View package NPM ## API# * * * `useLink( (props: AriaLinkOptions , , ref: RefObject <FocusableElement | | null> )): LinkAria ` ## Features# * * * Links can be created in HTML with the <a> element with an `href` attribute. However, if the link does not have an href, and is handled client side with JavaScript instead, it will not be exposed to assistive technology properly. `useLink` helps achieve accessible links with either native HTML elements or custom element types. * Support for mouse, touch, and keyboard interactions * Support for navigation links via `<a>` elements or custom element types via ARIA * Support for disabled links ## Anatomy# * * * A link consists of a pressable area usually containing a textual label or an icon that users can click or tap to navigate to another page or resource. In addition, keyboard users may activate links using the Enter key. `useLink` returns props to be spread onto the link element: | Name | Type | Description | | --- | --- | --- | | `linkProps` | `DOMAttributes` | Props for the link element. | | `isPressed` | `boolean` | Whether the link is currently pressed. | If a visual label is not provided (e.g. an icon or image only link), then an `aria-label` or `aria-labelledby` prop must be passed to identify the link to assistive technology. ## Example# * * * This example shows a basic link using a native `<a>` element. import {useLink} from 'react-aria'; function Link(props) { let ref = React.useRef(null); let { linkProps } = useLink(props, ref); return ( <a {...linkProps} ref={ref} style={{ color: 'var(--blue)' }} > {props.children} </a> ); } <Link href="https://adobe.com" target="_blank">Adobe</Link> import {useLink} from 'react-aria'; function Link(props) { let ref = React.useRef(null); let { linkProps } = useLink(props, ref); return ( <a {...linkProps} ref={ref} style={{ color: 'var(--blue)' }} > {props.children} </a> ); } <Link href="https://adobe.com" target="_blank"> Adobe </Link> import {useLink} from 'react-aria'; function Link(props) { let ref = React.useRef( null ); let { linkProps } = useLink(props, ref); return ( <a {...linkProps} ref={ref} style={{ color: 'var(--blue)' }} > {props.children} </a> ); } <Link href="https://adobe.com" target="_blank" > Adobe </Link> Adobe ## Client handled links# * * * This example shows a client handled link using press events. It sets `elementType` to `span` so that `useLink` returns the proper ARIA attributes to expose the element as a link to assistive technology. In addition, this example shows usage of the `isPressed` value returned by `useLink` to properly style the links's active state. You could use the CSS `:active` pseudo class for this, but `isPressed` properly handles when the user drags their pointer off of the link, along with keyboard support and better touch screen support. function Link(props) { let ref = React.useRef(null); let { linkProps, isPressed } = useLink( { ...props, elementType: 'span' }, ref ); return ( <span {...linkProps} ref={ref} style={{ color: isPressed ? 'var(--blue)' : 'var(--spectrum-global-color-blue-700)', textDecoration: 'underline', cursor: 'pointer' }} > {props.children} </span> ); } <Link onPress={() => alert('Pressed link')}>Adobe</Link> function Link(props) { let ref = React.useRef(null); let { linkProps, isPressed } = useLink( { ...props, elementType: 'span' }, ref ); return ( <span {...linkProps} ref={ref} style={{ color: isPressed ? 'var(--blue)' : 'var(--spectrum-global-color-blue-700)', textDecoration: 'underline', cursor: 'pointer' }} > {props.children} </span> ); } <Link onPress={() => alert('Pressed link')}>Adobe</Link> function Link(props) { let ref = React.useRef( null ); let { linkProps, isPressed } = useLink({ ...props, elementType: 'span' }, ref); return ( <span {...linkProps} ref={ref} style={{ color: isPressed ? 'var(--blue)' : 'var(--spectrum-global-color-blue-700)', textDecoration: 'underline', cursor: 'pointer' }} > {props.children} </span> ); } <Link onPress={() => alert( 'Pressed link' )} > Adobe </Link> Adobe ## Disabled links# * * * A link can be disabled by passing the `isDisabled` property. This will work with both native link elements as well as client handled links. Native navigation will be disabled, and the `onPress` event will not be fired. The link will be exposed as disabled to assistive technology with ARIA. function Link(props) { let ref = React.useRef(null); let { linkProps } = useLink(props, ref); return ( <a {...linkProps} ref={ref} style={{ color: props.isDisabled ? 'var(--gray)' : 'var(--blue)', cursor: props.isDisabled ? 'default' : 'pointer' }} > {props.children} </a> ); } <Link href="https://adobe.com" target="_blank" isDisabled>Disabled link</Link> function Link(props) { let ref = React.useRef(null); let { linkProps } = useLink(props, ref); return ( <a {...linkProps} ref={ref} style={{ color: props.isDisabled ? 'var(--gray)' : 'var(--blue)', cursor: props.isDisabled ? 'default' : 'pointer' }} > {props.children} </a> ); } <Link href="https://adobe.com" target="_blank" isDisabled> Disabled link </Link> function Link(props) { let ref = React.useRef( null ); let { linkProps } = useLink(props, ref); return ( <a {...linkProps} ref={ref} style={{ color: props .isDisabled ? 'var(--gray)' : 'var(--blue)', cursor: props .isDisabled ? 'default' : 'pointer' }} > {props.children} </a> ); } <Link href="https://adobe.com" target="_blank" isDisabled > Disabled link </Link> Disabled link --- ## Page: https://react-spectrum.adobe.com/react-aria/useTabList.html # useTabList Provides the behavior and accessibility implementation for a tab list. Tabs organize content into multiple sections and allow users to navigate between them. <table><tbody><tr><th>install</th><td><code>yarn add react-aria</code></td></tr><tr><th>version</th><td>3.41.1</td></tr><tr><th>usage</th><td><code><span>import</span> {useTabList, useTab, useTabPanel} <span>from</span> <span>'react-aria'</span></code></td></tr></tbody></table> View ARIA pattern W3C View repository GitHub View package NPM ## API# * * * `useTabList<T>( props: AriaTabListOptions <T>, state: TabListState <T>, ref: RefObject <HTMLElement | | null> ): TabListAria ``useTab<T>( props: AriaTabProps , state: TabListState <T>, ref: RefObject <FocusableElement | | null> ): TabAria ``useTabPanel<T>( props: AriaTabPanelProps , state: TabListState <T> | | null, ref: RefObject <Element | | null> ): TabPanelAria ` ## Features# * * * Tabs provide a list of tabs that a user can select from to switch between multiple tab panels. `useTabList`, `useTab`, and `useTabPanel` can be used to implement these in an accessible way. * Support for mouse, touch, and keyboard interactions on tabs * Support for LTR and RTL keyboard navigation * Support for disabled tabs * Follows the tabs ARIA pattern, semantically linking tabs and their associated tab panels * Focus management for tab panels without any focusable children ## Anatomy# * * * Tabs consist of a tab list with one or more visually separated tabs. Each tab has associated content, and only the selected tab's content is shown. Each tab can be clicked, tapped, or navigated to via arrow keys. Depending on the `keyboardActivation` prop, the tab can be selected by receiving keyboard focus, or it can be selected with the Enter key. `useTabList` returns props to spread onto the tab list container: | Name | Type | Description | | --- | --- | --- | | `tabListProps` | `DOMAttributes` | Props for the tablist container. | `useTab` returns props to be spread onto each individual tab, along with states that can be used for styling: | Name | Type | Description | | --- | --- | --- | | `tabProps` | `DOMAttributes` | Props for the tab element. | | `isSelected` | `boolean` | Whether the tab is currently selected. | | `isDisabled` | `boolean` | Whether the tab is disabled. | | `isPressed` | `boolean` | Whether the tab is currently in a pressed state. | `useTabPanel` returns props to spread onto the container for the tab content: | Name | Type | Description | | --- | --- | --- | | `tabPanelProps` | `DOMAttributes` | Props for the tab panel element. | State is managed by the `useTabListState` hook in `@react-stately/tabs`. The state object should be passed as an option to `useTabList`, `useTab`, and `useTabPanel`. The` Item `component is used to represent each tab, following the Collections API used by many other components. ## Example# * * * This example displays a basic list of tabs. The currently selected tab receives a `tabIndex` of 0 while the rest are set to -1 ensuring that the whole tablist is a single tab stop. The selected tab has a different style so it's obvious which one is currently selected. `useTab` and `useTabPanel` handle associating the tabs and tab panels for assistive technology. The currently selected tab panel is rendered below the list of tabs. The `key` prop on the `TabPanel` element is important to ensure that DOM state (e.g. text field contents) is not shared between unrelated tabs. import {useTab, useTabList, useTabPanel} from 'react-aria'; import {Item, useTabListState} from 'react-stately'; function Tabs(props) { let state = useTabListState(props); let ref = React.useRef(null); let { tabListProps } = useTabList(props, state, ref); return ( <div className={`tabs ${props.orientation || ''}`}> <div {...tabListProps} ref={ref}> {[...state.collection].map((item) => ( <Tab key={item.key} item={item} state={state} /> ))} </div> <TabPanel key={state.selectedItem?.key} state={state} /> </div> ); } function Tab({ item, state }) { let { key, rendered } = item; let ref = React.useRef(null); let { tabProps } = useTab({ key }, state, ref); return ( <div {...tabProps} ref={ref}> {rendered} </div> ); } function TabPanel({ state, ...props }) { let ref = React.useRef(null); let { tabPanelProps } = useTabPanel(props, state, ref); return ( <div {...tabPanelProps} ref={ref}> {state.selectedItem?.props.children} </div> ); } <Tabs aria-label="History of Ancient Rome"> <Item key="FoR" title="Founding of Rome"> Arma virumque cano, Troiae qui primus ab oris. </Item> <Item key="MaR" title="Monarchy and Republic"> Senatus Populusque Romanus. </Item> <Item key="Emp" title="Empire">Alea jacta est.</Item> </Tabs> import {useTab, useTabList, useTabPanel} from 'react-aria'; import {Item, useTabListState} from 'react-stately'; function Tabs(props) { let state = useTabListState(props); let ref = React.useRef(null); let { tabListProps } = useTabList(props, state, ref); return ( <div className={`tabs ${props.orientation || ''}`}> <div {...tabListProps} ref={ref}> {[...state.collection].map((item) => ( <Tab key={item.key} item={item} state={state} /> ))} </div> <TabPanel key={state.selectedItem?.key} state={state} /> </div> ); } function Tab({ item, state }) { let { key, rendered } = item; let ref = React.useRef(null); let { tabProps } = useTab({ key }, state, ref); return ( <div {...tabProps} ref={ref}> {rendered} </div> ); } function TabPanel({ state, ...props }) { let ref = React.useRef(null); let { tabPanelProps } = useTabPanel(props, state, ref); return ( <div {...tabPanelProps} ref={ref}> {state.selectedItem?.props.children} </div> ); } <Tabs aria-label="History of Ancient Rome"> <Item key="FoR" title="Founding of Rome"> Arma virumque cano, Troiae qui primus ab oris. </Item> <Item key="MaR" title="Monarchy and Republic"> Senatus Populusque Romanus. </Item> <Item key="Emp" title="Empire">Alea jacta est.</Item> </Tabs> import { useTab, useTabList, useTabPanel } from 'react-aria'; import { Item, useTabListState } from 'react-stately'; function Tabs(props) { let state = useTabListState( props ); let ref = React.useRef( null ); let { tabListProps } = useTabList( props, state, ref ); return ( <div className={`tabs ${ props .orientation || '' }`} > <div {...tabListProps} ref={ref} > {[ ...state .collection ].map((item) => ( <Tab key={item .key} item={item} state={state} /> ))} </div> <TabPanel key={state .selectedItem ?.key} state={state} /> </div> ); } function Tab( { item, state } ) { let { key, rendered } = item; let ref = React.useRef( null ); let { tabProps } = useTab( { key }, state, ref ); return ( <div {...tabProps} ref={ref} > {rendered} </div> ); } function TabPanel( { state, ...props } ) { let ref = React.useRef( null ); let { tabPanelProps } = useTabPanel( props, state, ref ); return ( <div {...tabPanelProps} ref={ref} > {state.selectedItem ?.props.children} </div> ); } <Tabs aria-label="History of Ancient Rome"> <Item key="FoR" title="Founding of Rome" > Arma virumque cano, Troiae qui primus ab oris. </Item> <Item key="MaR" title="Monarchy and Republic" > Senatus Populusque Romanus. </Item> <Item key="Emp" title="Empire" > Alea jacta est. </Item> </Tabs> Founding of Rome Monarchy and Republic Empire Arma virumque cano, Troiae qui primus ab oris. Show CSS .tabs { height: 150px; display: flex; flex-direction: column; } .tabs.vertical { flex-direction: row; } [role=tablist] { display: flex; } [role=tablist][aria-orientation=horizontal] { border-bottom: 1px solid gray; } [role=tablist][aria-orientation=vertical] { flex-direction: column; border-right: 1px solid gray; } [role=tab] { padding: 10px; cursor: default; } [role=tablist][aria-orientation=horizontal] [role=tab] { border-bottom: 3px solid transparent; } [role=tablist][aria-orientation=vertical] [role=tab] { border-right: 3px solid transparent; } [role=tablist] [role=tab][aria-selected=true] { border-color: var(--blue); } [role=tab][aria-disabled] { opacity: 0.5; } [role=tabpanel] { padding: 10px; } .tabs { height: 150px; display: flex; flex-direction: column; } .tabs.vertical { flex-direction: row; } [role=tablist] { display: flex; } [role=tablist][aria-orientation=horizontal] { border-bottom: 1px solid gray; } [role=tablist][aria-orientation=vertical] { flex-direction: column; border-right: 1px solid gray; } [role=tab] { padding: 10px; cursor: default; } [role=tablist][aria-orientation=horizontal] [role=tab] { border-bottom: 3px solid transparent; } [role=tablist][aria-orientation=vertical] [role=tab] { border-right: 3px solid transparent; } [role=tablist] [role=tab][aria-selected=true] { border-color: var(--blue); } [role=tab][aria-disabled] { opacity: 0.5; } [role=tabpanel] { padding: 10px; } .tabs { height: 150px; display: flex; flex-direction: column; } .tabs.vertical { flex-direction: row; } [role=tablist] { display: flex; } [role=tablist][aria-orientation=horizontal] { border-bottom: 1px solid gray; } [role=tablist][aria-orientation=vertical] { flex-direction: column; border-right: 1px solid gray; } [role=tab] { padding: 10px; cursor: default; } [role=tablist][aria-orientation=horizontal] [role=tab] { border-bottom: 3px solid transparent; } [role=tablist][aria-orientation=vertical] [role=tab] { border-right: 3px solid transparent; } [role=tablist] [role=tab][aria-selected=true] { border-color: var(--blue); } [role=tab][aria-disabled] { opacity: 0.5; } [role=tabpanel] { padding: 10px; } ## Styled examples# * * * Animated Selection A TabList component with an animated selection indicator. ## Usage# * * * The following examples show how to use the `Tabs` component created in the above example. ### Default selection# A default selected tab can be provided using the `defaultSelectedKey` prop, which should correspond to the `key` prop provided to each item. When `Tabs` is used with dynamic items as described below, the key of each item is derived from the data. See the `react-stately` Selection docs for more details. <Tabs aria-label="Input settings" defaultSelectedKey="keyboard"> <Item key="mouse">Mouse Settings</Item> <Item key="keyboard">Keyboard Settings</Item> <Item key="gamepad">Gamepad Settings</Item> </Tabs> <Tabs aria-label="Input settings" defaultSelectedKey="keyboard" > <Item key="mouse">Mouse Settings</Item> <Item key="keyboard">Keyboard Settings</Item> <Item key="gamepad">Gamepad Settings</Item> </Tabs> <Tabs aria-label="Input settings" defaultSelectedKey="keyboard" > <Item key="mouse"> Mouse Settings </Item> <Item key="keyboard"> Keyboard Settings </Item> <Item key="gamepad"> Gamepad Settings </Item> </Tabs> Mouse Settings Keyboard Settings Gamepad Settings Keyboard Settings ### Controlled selection# Selection can be controlled using the `selectedKey` prop, paired with the `onSelectionChange` event. The `key` prop from the selected tab will be passed into the callback when the tab is selected, allowing you to update state accordingly. function Example() { let [timePeriod, setTimePeriod] = React.useState('triassic'); return ( <> <p>Selected time period: {timePeriod}</p> <Tabs aria-label="Mesozoic time periods" selectedKey={timePeriod} onSelectionChange={setTimePeriod} > <Item key="triassic" title="Triassic"> The Triassic ranges roughly from 252 million to 201 million years ago, preceding the Jurassic Period. </Item> <Item key="jurassic" title="Jurassic"> The Jurassic ranges from 200 million years to 145 million years ago. </Item> <Item key="cretaceous" title="Cretaceous"> The Cretaceous is the longest period of the Mesozoic, spanning from 145 million to 66 million years ago. </Item> </Tabs> </> ); } function Example() { let [timePeriod, setTimePeriod] = React.useState( 'triassic' ); return ( <> <p>Selected time period: {timePeriod}</p> <Tabs aria-label="Mesozoic time periods" selectedKey={timePeriod} onSelectionChange={setTimePeriod} > <Item key="triassic" title="Triassic"> The Triassic ranges roughly from 252 million to 201 million years ago, preceding the Jurassic Period. </Item> <Item key="jurassic" title="Jurassic"> The Jurassic ranges from 200 million years to 145 million years ago. </Item> <Item key="cretaceous" title="Cretaceous"> The Cretaceous is the longest period of the Mesozoic, spanning from 145 million to 66 million years ago. </Item> </Tabs> </> ); } function Example() { let [ timePeriod, setTimePeriod ] = React.useState( 'triassic' ); return ( <> <p> Selected time period:{' '} {timePeriod} </p> <Tabs aria-label="Mesozoic time periods" selectedKey={timePeriod} onSelectionChange={setTimePeriod} > <Item key="triassic" title="Triassic" > The Triassic ranges roughly from 252 million to 201 million years ago, preceding the Jurassic Period. </Item> <Item key="jurassic" title="Jurassic" > The Jurassic ranges from 200 million years to 145 million years ago. </Item> <Item key="cretaceous" title="Cretaceous" > The Cretaceous is the longest period of the Mesozoic, spanning from 145 million to 66 million years ago. </Item> </Tabs> </> ); } Selected time period: triassic Triassic Jurassic Cretaceous The Triassic ranges roughly from 252 million to 201 million years ago, preceding the Jurassic Period. ### Focusable content# When the tab panel doesn't contain any focusable content, the entire panel is given a `tabIndex=0` so that the content can be navigated to with the keyboard. When the tab panel contains focusable content, such as a textfield, then the `tabIndex` is omitted because the content itself can receive focus. This example uses the same `Tabs` component from above. Try navigating from the tabs to the content for each panel using the keyboard. <Tabs aria-label="Notes app"> <Item key="item1" title="Jane Doe"> <label>Leave a note for Jane: <input type="text" /></label> </Item> <Item key="item2" title="John Doe">Senatus Populusque Romanus.</Item> <Item key="item3" title="Joe Bloggs">Alea jacta est.</Item> </Tabs> <Tabs aria-label="Notes app"> <Item key="item1" title="Jane Doe"> <label> Leave a note for Jane: <input type="text" /> </label> </Item> <Item key="item2" title="John Doe"> Senatus Populusque Romanus. </Item> <Item key="item3" title="Joe Bloggs"> Alea jacta est. </Item> </Tabs> <Tabs aria-label="Notes app"> <Item key="item1" title="Jane Doe" > <label> Leave a note for Jane:{' '} <input type="text" /> </label> </Item> <Item key="item2" title="John Doe" > Senatus Populusque Romanus. </Item> <Item key="item3" title="Joe Bloggs" > Alea jacta est. </Item> </Tabs> Jane Doe John Doe Joe Bloggs Leave a note for Jane: ### Dynamic items# The above examples have shown tabs with static items. The `items` prop can be used when creating tabs from a dynamic collection, for example when the user can add and remove tabs, or the tabs come from an external data source. The function passed as the children of the `Tabs` component is called for each item in the list, and returns an `<Item>` representing the tab. Each item accepts a `key` prop, which is passed to the `onSelectionChange` handler to identify the selected item. Alternatively, if the item objects contain an `id` property, as shown in the example below, then this is used automatically and a `key` prop is not required. See Collection Components for more details. function Example() { let [tabs, setTabs] = React.useState([ {id: 1, title: 'Tab 1', content: 'Tab body 1'}, {id: 2, title: 'Tab 2', content: 'Tab body 2'}, {id: 3, title: 'Tab 3', content: 'Tab body 3'} ]); let addTab = () => { setTabs(tabs => [ ...tabs, { id: tabs.length + 1, title: `Tab ${tabs.length + 1}`, content: `Tab Body ${tabs.length + 1}` } ]); }; let removeTab = () => { if (tabs.length > 1) { setTabs(tabs => tabs.slice(0, -1)); } }; return ( <> <button onClick={addTab}>Add tab</button> <button onClick={removeTab}>Remove tab</button> <Tabs aria-label="Dynamic tabs" items={tabs}> {item => <Item title={item.title}>{item.content}</Item>} </Tabs> </> ); } function Example() { let [tabs, setTabs] = React.useState([ { id: 1, title: 'Tab 1', content: 'Tab body 1' }, { id: 2, title: 'Tab 2', content: 'Tab body 2' }, { id: 3, title: 'Tab 3', content: 'Tab body 3' } ]); let addTab = () => { setTabs((tabs) => [ ...tabs, { id: tabs.length + 1, title: `Tab ${tabs.length + 1}`, content: `Tab Body ${tabs.length + 1}` } ]); }; let removeTab = () => { if (tabs.length > 1) { setTabs((tabs) => tabs.slice(0, -1)); } }; return ( <> <button onClick={addTab}>Add tab</button> <button onClick={removeTab}>Remove tab</button> <Tabs aria-label="Dynamic tabs" items={tabs}> {(item) => ( <Item title={item.title}>{item.content}</Item> )} </Tabs> </> ); } function Example() { let [tabs, setTabs] = React.useState([ { id: 1, title: 'Tab 1', content: 'Tab body 1' }, { id: 2, title: 'Tab 2', content: 'Tab body 2' }, { id: 3, title: 'Tab 3', content: 'Tab body 3' } ]); let addTab = () => { setTabs((tabs) => [ ...tabs, { id: tabs.length + 1, title: `Tab ${ tabs.length + 1 }`, content: `Tab Body ${ tabs.length + 1 }` } ]); }; let removeTab = () => { if ( tabs.length > 1 ) { setTabs((tabs) => tabs.slice(0, -1) ); } }; return ( <> <button onClick={addTab} > Add tab </button> <button onClick={removeTab} > Remove tab </button> <Tabs aria-label="Dynamic tabs" items={tabs} > {(item) => ( <Item title={item .title} > {item .content} </Item> )} </Tabs> </> ); } Add tabRemove tab Tab 1 Tab 2 Tab 3 Tab body 1 ### Keyboard Activation# By default, pressing the arrow keys while focus is on a Tab will switch selection to the adjacent Tab in that direction, updating the content displayed accordingly. If you would like to prevent selection change from happening automatically you can set the `keyboardActivation` prop to "manual". This will prevent tab selection from changing on arrow key press, requiring a subsequent `Enter` or `Space` key press to confirm tab selection. <Tabs aria-label="Input settings" keyboardActivation="manual"> <Item key="mouse">Mouse Settings</Item> <Item key="keyboard">Keyboard Settings</Item> <Item key="gamepad">Gamepad Settings</Item> </Tabs> <Tabs aria-label="Input settings" keyboardActivation="manual" > <Item key="mouse">Mouse Settings</Item> <Item key="keyboard">Keyboard Settings</Item> <Item key="gamepad">Gamepad Settings</Item> </Tabs> <Tabs aria-label="Input settings" keyboardActivation="manual" > <Item key="mouse"> Mouse Settings </Item> <Item key="keyboard"> Keyboard Settings </Item> <Item key="gamepad"> Gamepad Settings </Item> </Tabs> Mouse Settings Keyboard Settings Gamepad Settings Mouse Settings ### Orientation# By default, tabs are horizontally oriented. The `orientation` prop can be set to `vertical` to change this. This does not affect keyboard navigation. You are responsible for styling your tabs accordingly. <Tabs aria-label="Chat log orientation example" orientation="vertical"> <Item key="item1" title="John Doe"> There is no prior chat history with John Doe. </Item> <Item key="item2" title="Jane Doe"> There is no prior chat history with Jane Doe. </Item> <Item key="item3" title="Joe Bloggs"> There is no prior chat history with Joe Bloggs. </Item> </Tabs> <Tabs aria-label="Chat log orientation example" orientation="vertical" > <Item key="item1" title="John Doe"> There is no prior chat history with John Doe. </Item> <Item key="item2" title="Jane Doe"> There is no prior chat history with Jane Doe. </Item> <Item key="item3" title="Joe Bloggs"> There is no prior chat history with Joe Bloggs. </Item> </Tabs> <Tabs aria-label="Chat log orientation example" orientation="vertical" > <Item key="item1" title="John Doe" > There is no prior chat history with John Doe. </Item> <Item key="item2" title="Jane Doe" > There is no prior chat history with Jane Doe. </Item> <Item key="item3" title="Joe Bloggs" > There is no prior chat history with Joe Bloggs. </Item> </Tabs> John Doe Jane Doe Joe Bloggs There is no prior chat history with John Doe. ### Disabled# All tabs can be disabled using the `isDisabled` prop. <Tabs aria-label="Input settings" isDisabled> <Item key="mouse">Mouse Settings</Item> <Item key="keyboard">Keyboard Settings</Item> <Item key="gamepad">Gamepad Settings</Item> </Tabs> <Tabs aria-label="Input settings" isDisabled> <Item key="mouse">Mouse Settings</Item> <Item key="keyboard">Keyboard Settings</Item> <Item key="gamepad">Gamepad Settings</Item> </Tabs> <Tabs aria-label="Input settings" isDisabled > <Item key="mouse"> Mouse Settings </Item> <Item key="keyboard"> Keyboard Settings </Item> <Item key="gamepad"> Gamepad Settings </Item> </Tabs> Mouse Settings Keyboard Settings Gamepad Settings Mouse Settings ### Disabled items# Individual tabs can be disabled using the `disabledKeys` prop. Each key in this list corresponds with the `key` prop passed to the `Item` component, or automatically derived from the values passed to the `items` prop. See Collections for more details. <Tabs aria-label="Input settings" disabledKeys={['gamepad']}> <Item key="mouse">Mouse Settings</Item> <Item key="keyboard">Keyboard Settings</Item> <Item key="gamepad">Gamepad Settings</Item> </Tabs> <Tabs aria-label="Input settings" disabledKeys={['gamepad']} > <Item key="mouse">Mouse Settings</Item> <Item key="keyboard">Keyboard Settings</Item> <Item key="gamepad">Gamepad Settings</Item> </Tabs> <Tabs aria-label="Input settings" disabledKeys={[ 'gamepad' ]} > <Item key="mouse"> Mouse Settings </Item> <Item key="keyboard"> Keyboard Settings </Item> <Item key="gamepad"> Gamepad Settings </Item> </Tabs> Mouse Settings Keyboard Settings Gamepad Settings Mouse Settings ### Links# Tabs may be rendered as links to different routes in your application. This can be achieved by passing the `href` prop to the `<Item>` component. You'll need to update the `Tab` component to render an `<a>` element when an `href` prop is passed to an item. function Tab({item, state}) { let ref = React.useRef(null); let {tabProps} = useTab({key: item.key}, state, ref); let ElementType = item.props.href ? 'a' : 'div'; return ( <ElementType {...tabProps} ref={ref}> {item.rendered} </ElementType> ); } function Tab({item, state}) { let ref = React.useRef(null); let {tabProps} = useTab({key: item.key}, state, ref); let ElementType = item.props.href ? 'a' : 'div'; return ( <ElementType {...tabProps} ref={ref}> {item.rendered} </ElementType> ); } function Tab( { item, state } ) { let ref = React.useRef( null ); let { tabProps } = useTab( { key: item.key }, state, ref ); let ElementType = item.props.href ? 'a' : 'div'; return ( <ElementType {...tabProps} ref={ref} > {item.rendered} </ElementType> ); } By default, links perform native browser navigation. However, you'll usually want to synchronize the selected tab with the URL from your client side router. This takes two steps: 1. Set up a` RouterProvider `at the root of your app. This will handle link navigation from all React Aria components using your framework or router. See the client side routing guide to learn how to set this up. 2. Use the `selectedKey` prop to set the selected tab based on the URL, as described above. This example uses React Router to setup routes for each tab and synchronize the selection with the URL. import {BrowserRouter, Route, Routes, useLocation, useNavigate} from 'react-router-dom'; import {RouterProvider} from 'react-aria'; function AppTabs() { let { pathname } = useLocation(); return ( <Tabs selectedKey={pathname}> <TabList aria-label="Tabs"> <Tab id="/" href="/">Home</Tab> <Tab id="/shared" href="/shared">Shared</Tab> <Tab id="/deleted" href="/deleted">Deleted</Tab> </TabList> <TabPanel id={pathname}> <Routes> <Route path="/" element={<HomePage />} /> <Route path="/shared" element={<SharedPage />} /> <Route path="/deleted" element={<DeletedPage />} /> </Routes> </TabPanel> </Tabs> ); } function App() { let navigate = useNavigate(); return ( <RouterProvider navigate={navigate}> <Routes> <Route path="/*" element={<AppTabs />} /> </Routes> </RouterProvider> ); } <BrowserRouter> <App /> </BrowserRouter> import { BrowserRouter, Route, Routes, useLocation, useNavigate } from 'react-router-dom'; import {RouterProvider} from 'react-aria'; function AppTabs() { let { pathname } = useLocation(); return ( <Tabs selectedKey={pathname}> <TabList aria-label="Tabs"> <Tab id="/" href="/">Home</Tab> <Tab id="/shared" href="/shared">Shared</Tab> <Tab id="/deleted" href="/deleted">Deleted</Tab> </TabList> <TabPanel id={pathname}> <Routes> <Route path="/" element={<HomePage />} /> <Route path="/shared" element={<SharedPage />} /> <Route path="/deleted" element={<DeletedPage />} /> </Routes> </TabPanel> </Tabs> ); } function App() { let navigate = useNavigate(); return ( <RouterProvider navigate={navigate}> <Routes> <Route path="/*" element={<AppTabs />} /> </Routes> </RouterProvider> ); } <BrowserRouter> <App /> </BrowserRouter> import { BrowserRouter, Route, Routes, useLocation, useNavigate } from 'react-router-dom'; import {RouterProvider} from 'react-aria'; function AppTabs() { let { pathname } = useLocation(); return ( <Tabs selectedKey={pathname} > <TabList aria-label="Tabs"> <Tab id="/" href="/" > Home </Tab> <Tab id="/shared" href="/shared" > Shared </Tab> <Tab id="/deleted" href="/deleted" > Deleted </Tab> </TabList> <TabPanel id={pathname} > <Routes> <Route path="/" element={ <HomePage /> } /> <Route path="/shared" element={ <SharedPage /> } /> <Route path="/deleted" element={ <DeletedPage /> } /> </Routes> </TabPanel> </Tabs> ); } function App() { let navigate = useNavigate(); return ( <RouterProvider navigate={navigate} > <Routes> <Route path="/*" element={ <AppTabs /> } /> </Routes> </RouterProvider> ); } <BrowserRouter> <App /> </BrowserRouter> ## Internationalization# * * * `useTabList` handles some aspects of internationalization automatically. For example, keyboard navigation is automatically mirrored for right-to-left languages. You are responsible for localizing all tab labels and content. ### RTL# In right-to-left languages, the tablist should be mirrored. The first tab is furthest right and the last tab is furthest left. Ensure that your CSS accounts for this. --- ## Page: https://react-spectrum.adobe.com/react-aria/useDialog.html # useDialog Provides the behavior and accessibility implementation for a dialog component. A dialog is an overlay shown above other content in an application. <table><tbody><tr><th>install</th><td><code>yarn add react-aria</code></td></tr><tr><th>version</th><td>3.41.1</td></tr><tr><th>usage</th><td><code><span>import</span> {useDialog} <span>from</span> <span>'react-aria'</span></code></td></tr></tbody></table> View ARIA pattern W3C View repository GitHub View package NPM ## API# * * * `useDialog( (props: AriaDialogProps , , ref: RefObject <FocusableElement | | null> )): DialogAria ` ## Features# * * * The HTML <dialog> element can be used to build dialogs. However, it is not yet widely supported across browsers, and building fully accessible custom dialogs from scratch is very difficult and error prone. `useDialog` helps achieve accessible dialogs that can be styled as needed. * **Flexible** – Dialogs can be used within a modal or popover to create many types of overlay elements. * **Accessible** – Exposed to assistive technology as a `dialog` or `alertdialog` with ARIA. The dialog is labeled by its title element, and content outside the dialog is hidden from assistive technologies while it is open. * **Focus management** – Focus is moved into the dialog on mount, and restored to the trigger element on unmount. While open, focus is contained within the dialog, preventing the user from tabbing outside. ## Anatomy# * * * A dialog consists of a container element and an optional title. `useDialog` handles exposing this to assistive technology using ARIA. It can be combined with `useModalOverlay` or` usePopover `, to create modal dialogs, popovers, and other types of overlays. `useDialog` returns props that you should spread onto the appropriate element: | Name | Type | Description | | --- | --- | --- | | `dialogProps` | `DOMAttributes` | Props for the dialog container element. | | `titleProps` | `DOMAttributes` | Props for the dialog title element. | If a dialog does not have a visible title element, an `aria-label` or `aria-labelledby` prop must be passed instead to identify the element to assistive technology. ## Example# * * * This example shows how to build a typical modal dialog, by combining `useDialog` with useModalOverlay. The code for the `Modal` component is available below. The `Dialog` component may also be used within a popover. See the docs for more details on overlay containers. import type {AriaDialogProps} from 'react-aria'; import {useDialog} from 'react-aria'; // Reuse the Button and Modal from your component library. See below for details. import {Button, Modal, ModalTrigger} from 'your-component-library'; interface DialogProps extends AriaDialogProps { title?: React.ReactNode; children: React.ReactNode; } function Dialog({ title, children, ...props }: DialogProps) { let ref = React.useRef(null); let { dialogProps, titleProps } = useDialog(props, ref); return ( <div {...dialogProps} ref={ref} style={{ padding: 30 }}> {title && ( <h3 {...titleProps} style={{ marginTop: 0 }}> {title} </h3> )} {children} </div> ); } <ModalTrigger label="Open Dialog"> {(close) => ( <Dialog title="Enter your name"> <form style={{ display: 'flex', flexDirection: 'column' }}> <label htmlFor="first-name">First Name:</label> <input id="first-name" /> <label htmlFor="last-name">Last Name:</label> <input id="last-name" /> <Button onPress={close} style={{ marginTop: 10 }} > Submit </Button> </form> </Dialog> )} </ModalTrigger> import type {AriaDialogProps} from 'react-aria'; import {useDialog} from 'react-aria'; // Reuse the Button and Modal from your component library. See below for details. import { Button, Modal, ModalTrigger } from 'your-component-library'; interface DialogProps extends AriaDialogProps { title?: React.ReactNode; children: React.ReactNode; } function Dialog( { title, children, ...props }: DialogProps ) { let ref = React.useRef(null); let { dialogProps, titleProps } = useDialog(props, ref); return ( <div {...dialogProps} ref={ref} style={{ padding: 30 }}> {title && ( <h3 {...titleProps} style={{ marginTop: 0 }}> {title} </h3> )} {children} </div> ); } <ModalTrigger label="Open Dialog"> {(close) => ( <Dialog title="Enter your name"> <form style={{ display: 'flex', flexDirection: 'column' }} > <label htmlFor="first-name">First Name:</label> <input id="first-name" /> <label htmlFor="last-name">Last Name:</label> <input id="last-name" /> <Button onPress={close} style={{ marginTop: 10 }} > Submit </Button> </form> </Dialog> )} </ModalTrigger> import type {AriaDialogProps} from 'react-aria'; import {useDialog} from 'react-aria'; // Reuse the Button and Modal from your component library. See below for details. import { Button, Modal, ModalTrigger } from 'your-component-library'; interface DialogProps extends AriaDialogProps { title?: React.ReactNode; children: React.ReactNode; } function Dialog( { title, children, ...props }: DialogProps ) { let ref = React.useRef( null ); let { dialogProps, titleProps } = useDialog( props, ref ); return ( <div {...dialogProps} ref={ref} style={{ padding: 30 }} > {title && ( <h3 {...titleProps} style={{ marginTop: 0 }} > {title} </h3> )} {children} </div> ); } <ModalTrigger label="Open Dialog"> {(close) => ( <Dialog title="Enter your name"> <form style={{ display: 'flex', flexDirection: 'column' }} > <label htmlFor="first-name"> First Name: </label> <input id="first-name" /> <label htmlFor="last-name"> Last Name: </label> <input id="last-name" /> <Button onPress={close} style={{ marginTop: 10 }} > Submit </Button> </form> </Dialog> )} </ModalTrigger> Open Dialog ### Modal# The `Modal` and `ModalTrigger` components render the dialog within a typical modal container with a partially transparent underlay. See useModalOverlay for more details. Show code import {Overlay, useModalOverlay, useOverlayTrigger} from 'react-aria'; import {useOverlayTriggerState} from 'react-stately'; function Modal({ state, children, ...props }) { let ref = React.useRef(null); let { modalProps, underlayProps } = useModalOverlay(props, state, ref); return ( <Overlay> <div style={{ position: 'fixed', zIndex: 100, top: 0, left: 0, bottom: 0, right: 0, background: 'rgba(0, 0, 0, 0.5)', display: 'flex', alignItems: 'center', justifyContent: 'center' }} {...underlayProps} > <div {...modalProps} ref={ref} style={{ background: 'var(--page-background)', border: '1px solid gray' }} > {children} </div> </div> </Overlay> ); } function ModalTrigger({ label, children, ...props }) { let state = useOverlayTriggerState(props); let { triggerProps, overlayProps } = useOverlayTrigger( { type: 'dialog' }, state ); return ( <> <Button {...triggerProps}>{label}</Button> {state.isOpen && ( <Modal state={state}> {React.cloneElement(children(state.close), overlayProps)} </Modal> )} </> ); } import { Overlay, useModalOverlay, useOverlayTrigger } from 'react-aria'; import {useOverlayTriggerState} from 'react-stately'; function Modal({ state, children, ...props }) { let ref = React.useRef(null); let { modalProps, underlayProps } = useModalOverlay( props, state, ref ); return ( <Overlay> <div style={{ position: 'fixed', zIndex: 100, top: 0, left: 0, bottom: 0, right: 0, background: 'rgba(0, 0, 0, 0.5)', display: 'flex', alignItems: 'center', justifyContent: 'center' }} {...underlayProps} > <div {...modalProps} ref={ref} style={{ background: 'var(--page-background)', border: '1px solid gray' }} > {children} </div> </div> </Overlay> ); } function ModalTrigger({ label, children, ...props }) { let state = useOverlayTriggerState(props); let { triggerProps, overlayProps } = useOverlayTrigger({ type: 'dialog' }, state); return ( <> <Button {...triggerProps}>{label}</Button> {state.isOpen && ( <Modal state={state}> {React.cloneElement( children(state.close), overlayProps )} </Modal> )} </> ); } import { Overlay, useModalOverlay, useOverlayTrigger } from 'react-aria'; import {useOverlayTriggerState} from 'react-stately'; function Modal( { state, children, ...props } ) { let ref = React.useRef( null ); let { modalProps, underlayProps } = useModalOverlay( props, state, ref ); return ( <Overlay> <div style={{ position: 'fixed', zIndex: 100, top: 0, left: 0, bottom: 0, right: 0, background: 'rgba(0, 0, 0, 0.5)', display: 'flex', alignItems: 'center', justifyContent: 'center' }} {...underlayProps} > <div {...modalProps} ref={ref} style={{ background: 'var(--page-background)', border: '1px solid gray' }} > {children} </div> </div> </Overlay> ); } function ModalTrigger( { label, children, ...props } ) { let state = useOverlayTriggerState( props ); let { triggerProps, overlayProps } = useOverlayTrigger({ type: 'dialog' }, state); return ( <> <Button {...triggerProps} > {label} </Button> {state.isOpen && ( <Modal state={state} > {React .cloneElement( children( state .close ), overlayProps )} </Modal> )} </> ); } ### Button# The `Button` component is used in the above example to open and close the dialog. It is built using the useButton hook, and can be shared with many other components. Show code import {useButton} from 'react-aria'; function Button(props) { let ref = React.useRef(null); let { buttonProps } = useButton(props, ref); return ( <button {...buttonProps} ref={ref} style={props.style}> {props.children} </button> ); } import {useButton} from 'react-aria'; function Button(props) { let ref = React.useRef(null); let { buttonProps } = useButton(props, ref); return ( <button {...buttonProps} ref={ref} style={props.style}> {props.children} </button> ); } import {useButton} from 'react-aria'; function Button(props) { let ref = React.useRef( null ); let { buttonProps } = useButton( props, ref ); return ( <button {...buttonProps} ref={ref} style={props.style} > {props.children} </button> ); } ## Styled examples# * * * Tailwind CSS An animated alert dialog using Tailwind and react-transition-group. --- ## Page: https://react-spectrum.adobe.com/react-aria/useModalOverlay.html # useModalOverlay Provides the behavior and accessibility implementation for a modal component. A modal is an overlay element which blocks interaction with elements outside it. <table><tbody><tr><th>install</th><td><code>yarn add react-aria</code></td></tr><tr><th>version</th><td>3.41.1</td></tr><tr><th>usage</th><td><code><span>import</span> {useModalOverlay} <span>from</span> <span>'react-aria'</span></code></td></tr></tbody></table> View ARIA pattern W3C View repository GitHub View package NPM ## API# * * * `useModalOverlay( props: AriaModalOverlayProps , state: OverlayTriggerState , ref: RefObject <HTMLElement | | null> ): ModalOverlayAria ` ## Features# * * * The HTML <dialog> element can be used to build modal overlays. However, it is not yet widely supported across browsers, and can be difficult to style and customize. `useModalOverlay`, helps achieve accessible modal overlays that can be styled as needed. * **Accessible** – Content outside the modal is hidden from assistive technologies while it is open. The modal optionally closes when interacting outside, or pressing the Escape key. * **Focus management** – Focus is moved into the modal on mount, and restored to the trigger element on unmount. While open, focus is contained within the modal, preventing the user from tabbing outside. * **Scroll locking** – Scrolling the page behind the modal is prevented while it is open, including in mobile browsers. **Note**: `useModalOverlay` only handles the overlay itself. It should be combined with useDialog to create fully accessible modal dialogs. Other overlays such as menus may also be placed in a modal overlay. ## Anatomy# * * * A modal overlay consists of an overlay container element, and an underlay. The overlay may contain a dialog, or another element such as a menu or listbox when used within a component such as a select or combobox. The underlay is typically a partially transparent element that covers the rest of the screen behind the overlay, and prevents the user from interacting with the elements behind it. `useModalOverlay` returns props that you should spread onto the overlay and underlay elements: | Name | Type | Description | | --- | --- | --- | | `modalProps` | `DOMAttributes` | Props for the modal element. | | `underlayProps` | `DOMAttributes` | Props for the underlay element. | State is managed by the `useOverlayTriggerState` hook in `@react-stately/overlays`. The state object should be passed as an argument to `useModalOverlay`. ## Example# * * * This example shows how to build a typical modal dialog, by combining `useModalOverlay` with useDialog. The `Dialog` component used in this example can also be reused within a popover or other types of overlays. The `Modal` component uses an < `Overlay` \> to render its contents in a React Portal at the end of the document body, which ensures it is not clipped by other elements. It also acts as a focus scope, containing focus within the modal and restoring it to the trigger when it unmounts.` useModalOverlay `handles preventing page scrolling while the modal is open, hiding content outside the modal from screen readers, and optionally closing it when the user interacts outside or presses the Escape key. import {Overlay, useModalOverlay} from 'react-aria'; function Modal({ state, children, ...props }) { let ref = React.useRef(null); let { modalProps, underlayProps } = useModalOverlay(props, state, ref); return ( <Overlay> <div style={{ position: 'fixed', zIndex: 100, top: 0, left: 0, bottom: 0, right: 0, background: 'rgba(0, 0, 0, 0.5)', display: 'flex', alignItems: 'center', justifyContent: 'center' }} {...underlayProps} > <div {...modalProps} ref={ref} style={{ background: 'var(--page-background)', border: '1px solid gray' }} > {children} </div> </div> </Overlay> ); } import {Overlay, useModalOverlay} from 'react-aria'; function Modal({ state, children, ...props }) { let ref = React.useRef(null); let { modalProps, underlayProps } = useModalOverlay( props, state, ref ); return ( <Overlay> <div style={{ position: 'fixed', zIndex: 100, top: 0, left: 0, bottom: 0, right: 0, background: 'rgba(0, 0, 0, 0.5)', display: 'flex', alignItems: 'center', justifyContent: 'center' }} {...underlayProps} > <div {...modalProps} ref={ref} style={{ background: 'var(--page-background)', border: '1px solid gray' }} > {children} </div> </div> </Overlay> ); } import { Overlay, useModalOverlay } from 'react-aria'; function Modal( { state, children, ...props } ) { let ref = React.useRef( null ); let { modalProps, underlayProps } = useModalOverlay( props, state, ref ); return ( <Overlay> <div style={{ position: 'fixed', zIndex: 100, top: 0, left: 0, bottom: 0, right: 0, background: 'rgba(0, 0, 0, 0.5)', display: 'flex', alignItems: 'center', justifyContent: 'center' }} {...underlayProps} > <div {...modalProps} ref={ref} style={{ background: 'var(--page-background)', border: '1px solid gray' }} > {children} </div> </div> </Overlay> ); } The below `ModalTrigger` component uses the `useOverlayTrigger` hook to show the modal when a button is pressed. It accepts a function as children, which is called with a callback that closes the modal. This can be used to implement a close button. import {useOverlayTrigger} from 'react-aria'; import {useOverlayTriggerState} from 'react-stately'; // Reuse the Button from your component library. See below for details. import {Button} from 'your-component-library'; function ModalTrigger({ label, children, ...props }) { let state = useOverlayTriggerState(props); let { triggerProps, overlayProps } = useOverlayTrigger( { type: 'dialog' }, state ); return ( <> <Button {...triggerProps}>Open Dialog</Button> {state.isOpen && ( <Modal {...props} state={state}> {React.cloneElement(children(state.close), overlayProps)} </Modal> )} </> ); } import {useOverlayTrigger} from 'react-aria'; import {useOverlayTriggerState} from 'react-stately'; // Reuse the Button from your component library. See below for details. import {Button} from 'your-component-library'; function ModalTrigger({ label, children, ...props }) { let state = useOverlayTriggerState(props); let { triggerProps, overlayProps } = useOverlayTrigger({ type: 'dialog' }, state); return ( <> <Button {...triggerProps}>Open Dialog</Button> {state.isOpen && ( <Modal {...props} state={state}> {React.cloneElement( children(state.close), overlayProps )} </Modal> )} </> ); } import {useOverlayTrigger} from 'react-aria'; import {useOverlayTriggerState} from 'react-stately'; // Reuse the Button from your component library. See below for details. import {Button} from 'your-component-library'; function ModalTrigger( { label, children, ...props } ) { let state = useOverlayTriggerState( props ); let { triggerProps, overlayProps } = useOverlayTrigger({ type: 'dialog' }, state); return ( <> <Button {...triggerProps} > Open Dialog </Button> {state.isOpen && ( <Modal {...props} state={state} > {React .cloneElement( children( state .close ), overlayProps )} </Modal> )} </> ); } Now, we can render an example modal containing a dialog, with a button that closes it using the function provided by `ModalTrigger`. // Reuse the Dialog from your component library. See below for details. import {Dialog} from 'your-component-library'; <ModalTrigger label="Open Dialog"> {close => <Dialog title="Enter your name"> <form style={{display: 'flex', flexDirection: 'column'}}> <label htmlFor="first-name">First Name:</label> <input id="first-name" /> <label htmlFor="last-name">Last Name:</label> <input id="last-name" /> <Button onPress={close} style={{marginTop: 10}}> Submit </Button> </form> </Dialog> } </ModalTrigger> // Reuse the Dialog from your component library. See below for details. import {Dialog} from 'your-component-library'; <ModalTrigger label="Open Dialog"> {(close) => ( <Dialog title="Enter your name"> <form style={{ display: 'flex', flexDirection: 'column' }} > <label htmlFor="first-name">First Name:</label> <input id="first-name" /> <label htmlFor="last-name">Last Name:</label> <input id="last-name" /> <Button onPress={close} style={{ marginTop: 10 }} > Submit </Button> </form> </Dialog> )} </ModalTrigger> // Reuse the Dialog from your component library. See below for details. import {Dialog} from 'your-component-library'; <ModalTrigger label="Open Dialog"> {(close) => ( <Dialog title="Enter your name"> <form style={{ display: 'flex', flexDirection: 'column' }} > <label htmlFor="first-name"> First Name: </label> <input id="first-name" /> <label htmlFor="last-name"> Last Name: </label> <input id="last-name" /> <Button onPress={close} style={{ marginTop: 10 }} > Submit </Button> </form> </Dialog> )} </ModalTrigger> Open Dialog ### Dialog# The `Dialog` component is rendered within the `ModalOverlay` component. It is built using the useDialog hook, and can also be used in other overlay containers such as popovers. Show code import type {AriaDialogProps} from 'react-aria'; import {useDialog} from 'react-aria'; interface DialogProps extends AriaDialogProps { title?: React.ReactNode; children: React.ReactNode; } function Dialog({ title, children, ...props }: DialogProps) { let ref = React.useRef(null); let { dialogProps, titleProps } = useDialog(props, ref); return ( <div {...dialogProps} ref={ref} style={{ padding: 30 }}> {title && ( <h3 {...titleProps} style={{ marginTop: 0 }}> {title} </h3> )} {children} </div> ); } import type {AriaDialogProps} from 'react-aria'; import {useDialog} from 'react-aria'; interface DialogProps extends AriaDialogProps { title?: React.ReactNode; children: React.ReactNode; } function Dialog( { title, children, ...props }: DialogProps ) { let ref = React.useRef(null); let { dialogProps, titleProps } = useDialog(props, ref); return ( <div {...dialogProps} ref={ref} style={{ padding: 30 }}> {title && ( <h3 {...titleProps} style={{ marginTop: 0 }}> {title} </h3> )} {children} </div> ); } import type {AriaDialogProps} from 'react-aria'; import {useDialog} from 'react-aria'; interface DialogProps extends AriaDialogProps { title?: React.ReactNode; children: React.ReactNode; } function Dialog( { title, children, ...props }: DialogProps ) { let ref = React.useRef( null ); let { dialogProps, titleProps } = useDialog( props, ref ); return ( <div {...dialogProps} ref={ref} style={{ padding: 30 }} > {title && ( <h3 {...titleProps} style={{ marginTop: 0 }} > {title} </h3> )} {children} </div> ); } ### Button# The `Button` component is used in the above example to toggle the popover. It is built using the useButton hook, and can be shared with many other components. Show code import {useButton} from 'react-aria'; function Button(props) { let ref = props.buttonRef; let { buttonProps } = useButton(props, ref); return ( <button {...buttonProps} ref={ref} style={props.style}> {props.children} </button> ); } import {useButton} from 'react-aria'; function Button(props) { let ref = props.buttonRef; let { buttonProps } = useButton(props, ref); return ( <button {...buttonProps} ref={ref} style={props.style}> {props.children} </button> ); } import {useButton} from 'react-aria'; function Button(props) { let ref = props.buttonRef; let { buttonProps } = useButton( props, ref ); return ( <button {...buttonProps} ref={ref} style={props.style} > {props.children} </button> ); } ## Usage# * * * The following examples show how to use the `Modal` and `ModalTrigger` components created in the above example. ### Dismissable# If your modal doesn't require the user to make a confirmation, you can set `isDismissable` on the `Modal`. This allows the user to click outside to close the dialog. <ModalTrigger isDismissable label="Open Dialog"> {() => <Dialog title="Notice"> Click outside to close this dialog. </Dialog> } </ModalTrigger> <ModalTrigger isDismissable label="Open Dialog"> {() => <Dialog title="Notice"> Click outside to close this dialog. </Dialog> } </ModalTrigger> <ModalTrigger isDismissable label="Open Dialog" > {() => ( <Dialog title="Notice"> Click outside to close this dialog. </Dialog> )} </ModalTrigger> Open Dialog ### Keyboard dismiss disabled# By default, modals can be closed by pressing the Escape key. This can be disabled with the `isKeyboardDismissDisabled` prop. <ModalTrigger isKeyboardDismissDisabled label="Open Dialog"> {close => <Dialog title="Notice"> <p>You must close this dialog using the button below.</p> <Button onPress={close}>Close</Button> </Dialog> } </ModalTrigger> <ModalTrigger isKeyboardDismissDisabled label="Open Dialog" > {(close) => ( <Dialog title="Notice"> <p> You must close this dialog using the button below. </p> <Button onPress={close}>Close</Button> </Dialog> )} </ModalTrigger> <ModalTrigger isKeyboardDismissDisabled label="Open Dialog" > {(close) => ( <Dialog title="Notice"> <p> You must close this dialog using the button below. </p> <Button onPress={close} > Close </Button> </Dialog> )} </ModalTrigger> Open Dialog --- ## Page: https://react-spectrum.adobe.com/react-aria/usePopover.html # usePopover Provides the behavior and accessibility implementation for a popover component. A popover is an overlay element positioned relative to a trigger. <table><tbody><tr><th>install</th><td><code>yarn add react-aria</code></td></tr><tr><th>version</th><td>3.41.1</td></tr><tr><th>usage</th><td><code><span>import</span> {usePopover} <span>from</span> <span>'react-aria'</span></code></td></tr></tbody></table> View ARIA pattern W3C View repository GitHub View package NPM ## API# * * * `usePopover( (props: AriaPopoverProps , , state: OverlayTriggerState )): PopoverAria ` ## Features# * * * There is no built in way to create popovers in HTML. `usePopover` , helps achieve accessible popovers that can be styled as needed. * **Accessible** – The trigger and popover are automatically associated semantically via ARIA. Content outside the popover is hidden from assistive technologies while it is open. The popover closes when interacting outside, or pressing the Escape key. * **Focus management** – Focus is moved into the popover on mount, and restored to the trigger element on unmount. * **Positioning** – The popover is positioned relative to the trigger element, and automatically flips and adjusts to avoid overlapping with the edge of the browser window. Scrolling is prevented outside the popover to avoid unintentionally repositioning or closing it. **Note**: `usePopover` only handles the overlay itself. It should be combined with useDialog to create fully accessible popovers. Other overlays such as menus may also be placed in a popover. ## Anatomy# * * * A popover consists of a trigger element (e.g. button) and an overlay, which is positioned relative to the trigger. The overlay may contain a dialog, or another element such as a menu or listbox when used within a component such as a select or combobox. `usePopover` returns props that you should spread onto the appropriate elements, as well as the computed placement of the popover relative to the trigger: | Name | Type | Description | | --- | --- | --- | | `popoverProps` | `DOMAttributes` | Props for the popover element. | | `arrowProps` | `DOMAttributes` | Props for the popover tip arrow if any. | | `underlayProps` | `DOMAttributes` | Props to apply to the underlay element, if any. | | `placement` | ` PlacementAxis | null` | Placement of the popover with respect to the trigger. | State is managed by the `useOverlayTriggerState` hook in `@react-stately/overlays`. The state object should be passed as an argument to `usePopover`. ## Example# * * * This example shows how to build a typical popover overlay that is positioned relative to a trigger button. The content of the popover is a dialog, built with `useDialog` . The `Dialog` component used in this example can also be reused within a modal or other types of overlays. The implementation is available below. The `Popover` component uses an < `Overlay` \> to render its contents in a React Portal at the end of the document body, which ensures it is not clipped by other elements. It also acts as a focus scope, containing focus within the popover and restoring it to the trigger when it unmounts.` usePopover `handles positioning the popover relative to the trigger element, and closing it when the user interacts outside or presses the Escape key. `usePopover` also hides content outside the popover from screen readers, which is important since the surrounding content won't be in context of the original trigger due to the portal. To allow screen reader users to dismiss the popover without a keyboard (e.g. on mobile), visually hidden <` DismissButton `\> elements are added at the start and end of the popover. An underlay is also used to prevent scrolling and interacting with elements outside the popover with a pointer, to avoid unintentially repositioning or closing it. import {DismissButton, Overlay, usePopover} from 'react-aria'; import type {AriaPopoverProps} from 'react-aria'; import type {OverlayTriggerState} from 'react-stately'; interface PopoverProps extends Omit<AriaPopoverProps, 'popoverRef'> { children: React.ReactNode; state: OverlayTriggerState; } function Popover({ children, state, offset = 8, ...props }: PopoverProps) { let popoverRef = React.useRef(null); let { popoverProps, underlayProps, arrowProps, placement } = usePopover({ ...props, offset, popoverRef }, state); return ( <Overlay> <div {...underlayProps} className="underlay" /> <div {...popoverProps} ref={popoverRef} className="popover" > <svg {...arrowProps} className="arrow" data-placement={placement} viewBox="0 0 12 12" > <path d="M0 0 L6 6 L12 0" /> </svg> <DismissButton onDismiss={state.close} /> {children} <DismissButton onDismiss={state.close} /> </div> </Overlay> ); } import { DismissButton, Overlay, usePopover } from 'react-aria'; import type {AriaPopoverProps} from 'react-aria'; import type {OverlayTriggerState} from 'react-stately'; interface PopoverProps extends Omit<AriaPopoverProps, 'popoverRef'> { children: React.ReactNode; state: OverlayTriggerState; } function Popover( { children, state, offset = 8, ...props }: PopoverProps ) { let popoverRef = React.useRef(null); let { popoverProps, underlayProps, arrowProps, placement } = usePopover({ ...props, offset, popoverRef }, state); return ( <Overlay> <div {...underlayProps} className="underlay" /> <div {...popoverProps} ref={popoverRef} className="popover" > <svg {...arrowProps} className="arrow" data-placement={placement} viewBox="0 0 12 12" > <path d="M0 0 L6 6 L12 0" /> </svg> <DismissButton onDismiss={state.close} /> {children} <DismissButton onDismiss={state.close} /> </div> </Overlay> ); } import { DismissButton, Overlay, usePopover } from 'react-aria'; import type {AriaPopoverProps} from 'react-aria'; import type {OverlayTriggerState} from 'react-stately'; interface PopoverProps extends Omit< AriaPopoverProps, 'popoverRef' > { children: React.ReactNode; state: OverlayTriggerState; } function Popover( { children, state, offset = 8, ...props }: PopoverProps ) { let popoverRef = React .useRef(null); let { popoverProps, underlayProps, arrowProps, placement } = usePopover({ ...props, offset, popoverRef }, state); return ( <Overlay> <div {...underlayProps} className="underlay" /> <div {...popoverProps} ref={popoverRef} className="popover" > <svg {...arrowProps} className="arrow" data-placement={placement} viewBox="0 0 12 12" > <path d="M0 0 L6 6 L12 0" /> </svg> <DismissButton onDismiss={state .close} /> {children} <DismissButton onDismiss={state .close} /> </div> </Overlay> ); } The above `Popover` component can be used as part of many different patterns, such as ComboBox, Select, and DatePicker. To use it standalone, we need a trigger element. The below `PopoverTrigger` component uses the `useOverlayTrigger` hook to trigger the popover when a button is pressed. This hook also ensures that the button and popover are semantically connected via ARIA. import {useOverlayTrigger} from 'react-aria'; import {useOverlayTriggerState} from 'react-stately'; // Reuse the Button from your component library. See below for details. import {Button} from 'your-component-library'; function PopoverTrigger({ label, children, ...props }) { let ref = React.useRef(null); let state = useOverlayTriggerState(props); let { triggerProps, overlayProps } = useOverlayTrigger( { type: 'dialog' }, state, ref ); return ( <> <Button {...triggerProps} buttonRef={ref}>{label}</Button> {state.isOpen && ( <Popover {...props} triggerRef={ref} state={state}> {React.cloneElement(children, overlayProps)} </Popover> )} </> ); } import {useOverlayTrigger} from 'react-aria'; import {useOverlayTriggerState} from 'react-stately'; // Reuse the Button from your component library. See below for details. import {Button} from 'your-component-library'; function PopoverTrigger({ label, children, ...props }) { let ref = React.useRef(null); let state = useOverlayTriggerState(props); let { triggerProps, overlayProps } = useOverlayTrigger( { type: 'dialog' }, state, ref ); return ( <> <Button {...triggerProps} buttonRef={ref}> {label} </Button> {state.isOpen && ( <Popover {...props} triggerRef={ref} state={state} > {React.cloneElement(children, overlayProps)} </Popover> )} </> ); } import {useOverlayTrigger} from 'react-aria'; import {useOverlayTriggerState} from 'react-stately'; // Reuse the Button from your component library. See below for details. import {Button} from 'your-component-library'; function PopoverTrigger( { label, children, ...props } ) { let ref = React.useRef( null ); let state = useOverlayTriggerState( props ); let { triggerProps, overlayProps } = useOverlayTrigger( { type: 'dialog' }, state, ref ); return ( <> <Button {...triggerProps} buttonRef={ref} > {label} </Button> {state.isOpen && ( <Popover {...props} triggerRef={ref} state={state} > {React .cloneElement( children, overlayProps )} </Popover> )} </> ); } Now, we can render an example popover containing a dialog. // Reuse the Dialog from your component library. See below for details. import {Dialog} from 'your-component-library'; <PopoverTrigger label="Open Popover"> <Dialog title="Popover title"> This is the content of the popover. </Dialog> </PopoverTrigger> // Reuse the Dialog from your component library. See below for details. import {Dialog} from 'your-component-library'; <PopoverTrigger label="Open Popover"> <Dialog title="Popover title"> This is the content of the popover. </Dialog> </PopoverTrigger> // Reuse the Dialog from your component library. See below for details. import {Dialog} from 'your-component-library'; <PopoverTrigger label="Open Popover"> <Dialog title="Popover title"> This is the content of the popover. </Dialog> </PopoverTrigger> Open Popover Show CSS .underlay { position: fixed; inset: 0; } .popover { background: var(--page-background); border: 1px solid var(--spectrum-global-color-gray-400); box-shadow: 0 8px 20px rgba(0 0 0 / 0.1); border-radius: 6px; } .arrow { position: absolute; fill: var(--page-background); stroke: var(--spectrum-global-color-gray-400); stroke-width: 1px; width: 12px; height: 12px; } .arrow[data-placement=top] { top: 100%; transform: translateX(-50%); } .arrow[data-placement=bottom] { bottom: 100%; transform: translateX(-50%) rotate(180deg); } .arrow[data-placement=left] { left: 100%; transform: translateY(-50%) rotate(-90deg); } .arrow[data-placement=right] { right: 100%; transform: translateY(-50%) rotate(90deg); } .underlay { position: fixed; inset: 0; } .popover { background: var(--page-background); border: 1px solid var(--spectrum-global-color-gray-400); box-shadow: 0 8px 20px rgba(0 0 0 / 0.1); border-radius: 6px; } .arrow { position: absolute; fill: var(--page-background); stroke: var(--spectrum-global-color-gray-400); stroke-width: 1px; width: 12px; height: 12px; } .arrow[data-placement=top] { top: 100%; transform: translateX(-50%); } .arrow[data-placement=bottom] { bottom: 100%; transform: translateX(-50%) rotate(180deg); } .arrow[data-placement=left] { left: 100%; transform: translateY(-50%) rotate(-90deg); } .arrow[data-placement=right] { right: 100%; transform: translateY(-50%) rotate(90deg); } .underlay { position: fixed; inset: 0; } .popover { background: var(--page-background); border: 1px solid var(--spectrum-global-color-gray-400); box-shadow: 0 8px 20px rgba(0 0 0 / 0.1); border-radius: 6px; } .arrow { position: absolute; fill: var(--page-background); stroke: var(--spectrum-global-color-gray-400); stroke-width: 1px; width: 12px; height: 12px; } .arrow[data-placement=top] { top: 100%; transform: translateX(-50%); } .arrow[data-placement=bottom] { bottom: 100%; transform: translateX(-50%) rotate(180deg); } .arrow[data-placement=left] { left: 100%; transform: translateY(-50%) rotate(-90deg); } .arrow[data-placement=right] { right: 100%; transform: translateY(-50%) rotate(90deg); } ### Dialog# The `Dialog` component is rendered within the `Popover` component. It is built using the useDialog hook, and can also be used in other overlay containers such as modals. Show code import type {AriaDialogProps} from 'react-aria'; import {useDialog} from 'react-aria'; interface DialogProps extends AriaDialogProps { title?: React.ReactNode; children: React.ReactNode; } function Dialog({ title, children, ...props }: DialogProps) { let ref = React.useRef(null); let { dialogProps, titleProps } = useDialog(props, ref); return ( <div {...dialogProps} ref={ref} style={{ padding: 30, maxWidth: 200, outline: 'none' }} > {title && ( <h3 {...titleProps} style={{ marginTop: 0 }}> {title} </h3> )} {children} </div> ); } import type {AriaDialogProps} from 'react-aria'; import {useDialog} from 'react-aria'; interface DialogProps extends AriaDialogProps { title?: React.ReactNode; children: React.ReactNode; } function Dialog( { title, children, ...props }: DialogProps ) { let ref = React.useRef(null); let { dialogProps, titleProps } = useDialog(props, ref); return ( <div {...dialogProps} ref={ref} style={{ padding: 30, maxWidth: 200, outline: 'none' }} > {title && ( <h3 {...titleProps} style={{ marginTop: 0 }}> {title} </h3> )} {children} </div> ); } import type {AriaDialogProps} from 'react-aria'; import {useDialog} from 'react-aria'; interface DialogProps extends AriaDialogProps { title?: React.ReactNode; children: React.ReactNode; } function Dialog( { title, children, ...props }: DialogProps ) { let ref = React.useRef( null ); let { dialogProps, titleProps } = useDialog( props, ref ); return ( <div {...dialogProps} ref={ref} style={{ padding: 30, maxWidth: 200, outline: 'none' }} > {title && ( <h3 {...titleProps} style={{ marginTop: 0 }} > {title} </h3> )} {children} </div> ); } ### Button# The `Button` component is used in the above example to toggle the popover. It is built using the useButton hook, and can be shared with many other components. Show code import {useButton} from 'react-aria'; function Button(props) { let ref = props.buttonRef; let { buttonProps } = useButton(props, ref); return ( <button {...buttonProps} ref={ref} style={props.style}> {props.children} </button> ); } import {useButton} from 'react-aria'; function Button(props) { let ref = props.buttonRef; let { buttonProps } = useButton(props, ref); return ( <button {...buttonProps} ref={ref} style={props.style}> {props.children} </button> ); } import {useButton} from 'react-aria'; function Button(props) { let ref = props.buttonRef; let { buttonProps } = useButton( props, ref ); return ( <button {...buttonProps} ref={ref} style={props.style} > {props.children} </button> ); } ## Usage# * * * The following examples show how to use the `Popover` and `PopoverTrigger` components created in the above example. ### Placement# The popover's placement with respect to its anchor element can be adjusted using the `placement` prop. See `Placement` for a full list of available placement combinations. <div style={{ display: 'flex', gap: 8 }}> <PopoverTrigger placement="start" label="⬅️"> <Dialog> In left-to-right, this is on the left. In right-to-left, this is on the right. </Dialog> </PopoverTrigger> <PopoverTrigger placement="top" label="⬆️"> <Dialog>This popover is above the button.</Dialog> </PopoverTrigger> <PopoverTrigger placement="bottom" label="⬇️"> <Dialog>This popover is below the button.</Dialog> </PopoverTrigger> <PopoverTrigger placement="end" label="➡️"> <Dialog> In left-to-right, this is on the right. In right-to-left, this is on the left. </Dialog> </PopoverTrigger> </div> <div style={{ display: 'flex', gap: 8 }}> <PopoverTrigger placement="start" label="⬅️"> <Dialog> In left-to-right, this is on the left. In right-to-left, this is on the right. </Dialog> </PopoverTrigger> <PopoverTrigger placement="top" label="⬆️"> <Dialog>This popover is above the button.</Dialog> </PopoverTrigger> <PopoverTrigger placement="bottom" label="⬇️"> <Dialog>This popover is below the button.</Dialog> </PopoverTrigger> <PopoverTrigger placement="end" label="➡️"> <Dialog> In left-to-right, this is on the right. In right-to-left, this is on the left. </Dialog> </PopoverTrigger> </div> <div style={{ display: 'flex', gap: 8 }} > <PopoverTrigger placement="start" label="⬅️" > <Dialog> In left-to-right, this is on the left. In right-to-left, this is on the right. </Dialog> </PopoverTrigger> <PopoverTrigger placement="top" label="⬆️" > <Dialog> This popover is above the button. </Dialog> </PopoverTrigger> <PopoverTrigger placement="bottom" label="⬇️" > <Dialog> This popover is below the button. </Dialog> </PopoverTrigger> <PopoverTrigger placement="end" label="➡️" > <Dialog> In left-to-right, this is on the right. In right-to-left, this is on the left. </Dialog> </PopoverTrigger> </div> ⬅️⬆️⬇️➡️ ### Offset and cross offset# The popover's offset with respect to its anchor element can be adjusted using the `offset` and `crossOffset` props. The `offset` prop controls the spacing applied along the main axis between the element and its anchor element whereas the `crossOffset` prop handles the spacing applied along the cross axis. Below is a popover offset by an additional 50px above the trigger. <PopoverTrigger placement="top" offset={50} label="Trigger"> <Dialog> Offset by an additional 50px. </Dialog> </PopoverTrigger> <PopoverTrigger placement="top" offset={50} label="Trigger"> <Dialog> Offset by an additional 50px. </Dialog> </PopoverTrigger> <PopoverTrigger placement="top" offset={50} label="Trigger" > <Dialog> Offset by an additional 50px. </Dialog> </PopoverTrigger> Trigger Below is a popover cross offset by an additional 100px to the right of the trigger. <PopoverTrigger placement="top" crossOffset={100} label="Trigger"> <Dialog> Offset by an additional 100px. </Dialog> </PopoverTrigger> <PopoverTrigger placement="top" crossOffset={100} label="Trigger" > <Dialog> Offset by an additional 100px. </Dialog> </PopoverTrigger> <PopoverTrigger placement="top" crossOffset={100} label="Trigger" > <Dialog> Offset by an additional 100px. </Dialog> </PopoverTrigger> Trigger ### Flipping# By default, `usePopover` attempts to flip popovers on the main axis in situations where the original placement would cause it to render out of view. This can be overridden by setting `shouldFlip={false}`. To see the difference between the two options, scroll this page so that the example below is near the bottom of the window. <PopoverTrigger placement="bottom" label="Default"> <Dialog> This is a popover that will flip if it can't fully render below the button. </Dialog> </PopoverTrigger> <PopoverTrigger placement="bottom" shouldFlip={false} label="shouldFlip=false"> <Dialog> This is a popover that won't flip if it can't fully render below the button. </Dialog> </PopoverTrigger> <PopoverTrigger placement="bottom" label="Default"> <Dialog> This is a popover that will flip if it can't fully render below the button. </Dialog> </PopoverTrigger> <PopoverTrigger placement="bottom" shouldFlip={false} label="shouldFlip=false" > <Dialog> This is a popover that won't flip if it can't fully render below the button. </Dialog> </PopoverTrigger> <PopoverTrigger placement="bottom" label="Default" > <Dialog> This is a popover that will flip if it can't fully render below the button. </Dialog> </PopoverTrigger> <PopoverTrigger placement="bottom" shouldFlip={false} label="shouldFlip=false" > <Dialog> This is a popover that won't flip if it can't fully render below the button. </Dialog> </PopoverTrigger> DefaultshouldFlip=false ### Container padding# You can control the minimum padding required between the popover and the surrounding container via the `containerPadding` prop. This affects the positioning breakpoints that determine when it will attempt to flip. The example below will maintain at least 50px between the popover and the edge of the browser window. <PopoverTrigger placement="top" containerPadding={50} label="Trigger"> <Dialog> This is a popover. </Dialog> </PopoverTrigger> <PopoverTrigger placement="top" containerPadding={50} label="Trigger" > <Dialog> This is a popover. </Dialog> </PopoverTrigger> <PopoverTrigger placement="top" containerPadding={50} label="Trigger" > <Dialog> This is a popover. </Dialog> </PopoverTrigger> Trigger --- ## Page: https://react-spectrum.adobe.com/react-aria/useTooltipTrigger.html # useTooltipTrigger Provides the behavior and accessibility implementation for a tooltip trigger, e.g. a button that shows a description when focused or hovered. <table><tbody><tr><th>install</th><td><code>yarn add react-aria</code></td></tr><tr><th>version</th><td>3.41.1</td></tr><tr><th>usage</th><td><code><span>import</span> {useTooltipTrigger, useTooltip} <span>from</span> <span>'react-aria'</span></code></td></tr></tbody></table> View ARIA pattern W3C View repository GitHub View package NPM ## API# * * * `useTooltipTrigger( props: TooltipTriggerProps , state: TooltipTriggerState , ref: RefObject <FocusableElement | | null> ): TooltipTriggerAria ``useTooltip( (props: AriaTooltipProps , , state?: TooltipTriggerState )): TooltipAria ` ## Features# * * * The HTML `title` attribute can be used to create a tooltip, but it cannot be styled. Custom styled tooltips can be hard to build in an accessible way. `useTooltipTrigger` and `useTooltip` help build fully accessible tooltips that can be styled as needed. * Keyboard focus management and cross browser normalization * Hover management and cross browser normalization * Labeling support for screen readers (aria-describedby) * Exposed as a tooltip to assistive technology via ARIA * Matches native tooltip behavior with delay on hover of first tooltip and no delay on subsequent tooltips. ## Anatomy# * * * A tooltip consists of two parts: the trigger element and the tooltip itself. Users may reveal the tooltip by hovering or focusing the trigger. `useTooltipTrigger` returns props to be spread onto its target trigger and the tooltip: | Name | Type | Description | | --- | --- | --- | | `triggerProps` | `DOMAttributes` | Props for the trigger element. | | `tooltipProps` | `DOMAttributes` | Props for the overlay container element. | `useTooltip` returns props to be spread onto the tooltip: | Name | Type | Description | | --- | --- | --- | | `tooltipProps` | `DOMAttributes` | Props for the tooltip element. | Tooltip state is managed by the `useTooltipTriggerState` hook in `@react-stately/tooltip`. The state object should be passed as an option to `useTooltipTrigger` and `useTooltip`. ## Example# * * * This example implements a Tooltip that renders in an absolute position next to its target. The `useTooltip` hook applies the correct ARIA attributes to the tooltip, and `useTooltipTrigger` associates it to the trigger element. Two instances of the example are rendered to demonstrate the behavior of the delay on hover. If you hover over the first button, the tooltip will be shown after a delay. Hovering over the second button shows the tooltip immediately. If you wait for a delay before hovering another element, the delay restarts. import {useTooltipTriggerState} from 'react-stately'; import {mergeProps, useTooltip, useTooltipTrigger} from 'react-aria'; function Tooltip({ state, ...props }) { let { tooltipProps } = useTooltip(props, state); return ( <span style={{ position: 'absolute', left: '5px', top: '100%', maxWidth: 150, marginTop: '10px', backgroundColor: 'white', color: 'black', padding: '5px', border: '1px solid gray' }} {...mergeProps(props, tooltipProps)} > {props.children} </span> ); } function TooltipButton(props) { let state = useTooltipTriggerState(props); let ref = React.useRef(null); // Get props for the trigger and its tooltip let { triggerProps, tooltipProps } = useTooltipTrigger(props, state, ref); return ( <span style={{ position: 'relative' }}> <button ref={ref} {...triggerProps} style={{ fontSize: 18 }} onClick={() => alert('Pressed button')} > {props.children} </button> {state.isOpen && ( <Tooltip state={state} {...tooltipProps}>{props.tooltip}</Tooltip> )} </span> ); } <TooltipButton tooltip="Edit">✏️</TooltipButton> <TooltipButton tooltip="Delete">🚮</TooltipButton> import {useTooltipTriggerState} from 'react-stately'; import { mergeProps, useTooltip, useTooltipTrigger } from 'react-aria'; function Tooltip({ state, ...props }) { let { tooltipProps } = useTooltip(props, state); return ( <span style={{ position: 'absolute', left: '5px', top: '100%', maxWidth: 150, marginTop: '10px', backgroundColor: 'white', color: 'black', padding: '5px', border: '1px solid gray' }} {...mergeProps(props, tooltipProps)} > {props.children} </span> ); } function TooltipButton(props) { let state = useTooltipTriggerState(props); let ref = React.useRef(null); // Get props for the trigger and its tooltip let { triggerProps, tooltipProps } = useTooltipTrigger( props, state, ref ); return ( <span style={{ position: 'relative' }}> <button ref={ref} {...triggerProps} style={{ fontSize: 18 }} onClick={() => alert('Pressed button')} > {props.children} </button> {state.isOpen && ( <Tooltip state={state} {...tooltipProps}> {props.tooltip} </Tooltip> )} </span> ); } <TooltipButton tooltip="Edit">✏️</TooltipButton> <TooltipButton tooltip="Delete">🚮</TooltipButton> import {useTooltipTriggerState} from 'react-stately'; import { mergeProps, useTooltip, useTooltipTrigger } from 'react-aria'; function Tooltip( { state, ...props } ) { let { tooltipProps } = useTooltip( props, state ); return ( <span style={{ position: 'absolute', left: '5px', top: '100%', maxWidth: 150, marginTop: '10px', backgroundColor: 'white', color: 'black', padding: '5px', border: '1px solid gray' }} {...mergeProps( props, tooltipProps )} > {props.children} </span> ); } function TooltipButton( props ) { let state = useTooltipTriggerState( props ); let ref = React.useRef( null ); // Get props for the trigger and its tooltip let { triggerProps, tooltipProps } = useTooltipTrigger( props, state, ref ); return ( <span style={{ position: 'relative' }} > <button ref={ref} {...triggerProps} style={{ fontSize: 18 }} onClick={() => alert( 'Pressed button' )} > {props.children} </button> {state.isOpen && ( <Tooltip state={state} {...tooltipProps} > {props.tooltip} </Tooltip> )} </span> ); } <TooltipButton tooltip="Edit"> ✏️ </TooltipButton> <TooltipButton tooltip="Delete"> 🚮 </TooltipButton> ✏️🚮 ## Usage# * * * The following examples show how to use the `TooltipButton` component created in the above example. ### Delay# Tooltips appear after a short delay when hovering the trigger, or instantly when using keyboard focus. This delay can be adjusted for hover using the `delay` prop. <TooltipButton tooltip="Save" delay={0}>💾</TooltipButton> <TooltipButton tooltip="Save" delay={0}>💾</TooltipButton> <TooltipButton tooltip="Save" delay={0} > 💾 </TooltipButton> 💾 ### Close Delay# Tooltips disappear after a short delay when no longer hovering the trigger, or instantly when using keyboard focus. This delay can be adjusted for hover using the `closeDelay` prop. <TooltipButton tooltip="Refresh" closeDelay={0}>🔄</TooltipButton> <TooltipButton tooltip="Refresh" closeDelay={0}> 🔄 </TooltipButton> <TooltipButton tooltip="Refresh" closeDelay={0} > 🔄 </TooltipButton> 🔄 ### Trigger# By default, tooltips appear both on hover and on focus. The `trigger` prop can be set to `"focus"` to display the tooltip only on focus, and not on hover. <TooltipButton tooltip="Burn CD" trigger="focus">💿</TooltipButton> <TooltipButton tooltip="Burn CD" trigger="focus"> 💿 </TooltipButton> <TooltipButton tooltip="Burn CD" trigger="focus" > 💿 </TooltipButton> 💿 ### Controlled open state# The open state of the tooltip can be controlled via the `defaultOpen` and `isOpen` props. function Example() { let [isOpen, setOpen] = React.useState(false); return ( <> <p>Tooltip is {isOpen ? 'showing' : 'not showing'}</p> <TooltipButton tooltip="Notifications" isOpen={isOpen} onOpenChange={setOpen} > 📣 </TooltipButton> </> ); } function Example() { let [isOpen, setOpen] = React.useState(false); return ( <> <p>Tooltip is {isOpen ? 'showing' : 'not showing'}</p> <TooltipButton tooltip="Notifications" isOpen={isOpen} onOpenChange={setOpen} > 📣 </TooltipButton> </> ); } function Example() { let [isOpen, setOpen] = React.useState( false ); return ( <> <p> Tooltip is{' '} {isOpen ? 'showing' : 'not showing'} </p> <TooltipButton tooltip="Notifications" isOpen={isOpen} onOpenChange={setOpen} > 📣 </TooltipButton> </> ); } Tooltip is not showing 📣 ### Disabled# The `isDisabled` prop can be provided to a TooltipTrigger to disable the tooltip, without disabling the trigger it displays on. <TooltipButton tooltip="Print" isDisabled>🖨</TooltipButton> <TooltipButton tooltip="Print" isDisabled>🖨</TooltipButton> <TooltipButton tooltip="Print" isDisabled > 🖨 </TooltipButton> 🖨 ## Internationalization# * * * ### RTL# In right-to-left languages, tooltips should be mirrored across trigger. Ensure that your CSS and overlay positioning (if any) accounts for this. --- ## Page: https://react-spectrum.adobe.com/react-aria/useAutocomplete.html beta # useAutocomplete Provides the behavior and accessibility implementation for an autocomplete component. An autocomplete combines a text input with a collection, allowing users to filter the collection's contents match a query. <table><tbody><tr><th>install</th><td><code>yarn add @react-aria/autocomplete</code></td></tr><tr><th>version</th><td>3.0.0-beta.5</td></tr><tr><th>usage</th><td><code><span>import</span> {useAutocomplete} <span>from</span> <span>'@react-aria/autocomplete'</span></code></td></tr></tbody></table> View repository GitHub View package NPM ### Under construction This hook is in **beta**. More documentation is coming soon! ## API# * * * `useAutocomplete( (props: AriaAutocompleteOptions , , state: AutocompleteState )): AutocompleteAria ` ## Features# * * * Autocomplete can be implemented using the <datalist> HTML element, but this has limited functionality and behaves differently across browsers. `useAutocomplete` helps achieve accessible text input and collection that can be styled as needed. ## Anatomy# * * * An autocomplete consists of a text input that displays the current value and a collection of items. Users can type within the input to filter the collection. `useAutocomplete` handles exposing the correct ARIA attributes for accessibility for each of the elements comprising the autocomplete. `useAutocomplete` returns props that you should spread onto the appropriate elements: | Name | Type | Description | | --- | --- | --- | | `textFieldProps` | ` AriaTextFieldProps ` | Props for the autocomplete textfield/searchfield element. These should be passed to the textfield/searchfield aria hooks respectively. | | `collectionProps` | ` CollectionOptions ` | Props for the collection, to be passed to collection's respective aria hook (e.g. useMenu). | | `collectionRef` | ` RefObject <HTMLElement | null>` | Ref to attach to the wrapped collection. | | `filter` | `( (nodeTextValue: string )) => boolean` | A filter function that returns if the provided collection node should be filtered out of the collection. | State is managed by the `useAutocompleteState` hook from `@react-stately/autocomplete`. The state object should be passed as an option to `useAutocomplete`. ## Internationalization# * * * `useAutocomplete` handles some aspects of internationalization automatically. For example, VoiceOver announcements about the item focus, count, and selection are localized. You are responsible for localizing all labels and option content that is passed into the autocomplete. --- ## Page: https://react-spectrum.adobe.com/react-aria/useComboBox.html # useComboBox Provides the behavior and accessibility implementation for a combo box component. A combo box combines a text input with a listbox, allowing users to filter a list of options to items matching a query. <table><tbody><tr><th>install</th><td><code>yarn add react-aria</code></td></tr><tr><th>version</th><td>3.41.1</td></tr><tr><th>usage</th><td><code><span>import</span> {useComboBox} <span>from</span> <span>'react-aria'</span></code></td></tr></tbody></table> View ARIA pattern W3C View repository GitHub View package NPM ## API# * * * `useComboBox<T>( (props: AriaComboBoxOptions <T>, , state: ComboBoxState <T> )): ComboBoxAria <T>` ## Features# * * * A combo box can be built using the <datalist> HTML element, but this is very limited in functionality and difficult to style. `useComboBox` helps achieve accessible combo box and autocomplete components that can be styled as needed. * Support for filtering a list of options by typing * Support for selecting a single option * Support for disabled options * Support for groups of items in sections * Support for custom user input values * Support for controlled and uncontrolled options, selection, input value, and open state * Support for custom filter functions * Async loading and infinite scrolling support * Support for use with virtualized lists * Exposed to assistive technology as a `combobox` with ARIA * Labeling support for accessibility * Required and invalid states exposed to assistive technology via ARIA * Support for mouse, touch, and keyboard interactions * Keyboard support for opening the combo box list box using the arrow keys, including automatically focusing the first or last item accordingly * Support for opening the list box when typing, on focus, or manually * Handles virtual clicks on the input from touch screen readers to toggle the list box * Virtual focus management for combo box list box option navigation * Hides elements outside the input and list box from assistive technology while the list box is open in a portal * Custom localized announcements for option focusing, filtering, and selection using an ARIA live region to work around VoiceOver bugs * Support for native HTML constraint validation with customizable UI, custom validation functions, realtime validation, and server-side validation errors Read our blog post for more details about the interactions and accessibility features implemented by `useComboBox`. ## Anatomy# * * * A combo box consists of a label, an input which displays the current value, a list box popup, and an optional button used to toggle the list box popup open state. Users can type within the input to filter the available options within the list box. The list box popup may be opened by a variety of input field interactions specified by the `menuTrigger` prop provided to `useComboBox`, or by clicking or touching the button. `useComboBox` handles exposing the correct ARIA attributes for accessibility for each of the elements comprising the combo box. It should be combined with useListBox, which handles the implementation of the popup list box, and useButton which handles the button press interactions. `useComboBox` also supports optional description and error message elements, which can be used to provide more context about the field, and any validation messages. These are linked with the input via the `aria-describedby` attribute. `useComboBox` returns props that you should spread onto the appropriate elements: | Name | Type | Description | | --- | --- | --- | | `labelProps` | `DOMAttributes` | Props for the label element. | | `inputProps` | `InputHTMLAttributes<HTMLInputElement>` | Props for the combo box input element. | | `listBoxProps` | ` AriaListBoxOptions <T>` | Props for the list box, to be passed to useListBox. | | `buttonProps` | ` AriaButtonProps ` | Props for the optional trigger button, to be passed to useButton. | | `descriptionProps` | `DOMAttributes` | Props for the combo box description element, if any. | | `errorMessageProps` | `DOMAttributes` | Props for the combo box error message element, if any. | | `isInvalid` | `boolean` | Whether the input value is invalid. | | `validationErrors` | `string[]` | The current error messages for the input if it is invalid, otherwise an empty array. | | `validationDetails` | `ValidityState` | The native validation details for the input. | State is managed by the `useComboBoxState` hook from `@react-stately/combobox`. The state object should be passed as an option to `useComboBox`. If the combo box does not have a visible label, an `aria-label` or `aria-labelledby` prop must be passed instead to identify it to assistive technology. ## State management# * * * `useComboBox` requires knowledge of the options in the combo box in order to handle keyboard navigation and other interactions. It does this using the `Collection` interface, which is a generic interface to access sequential unique keyed data. You can implement this interface yourself, e.g. by using a prop to pass a list of item objects, but` useComboBoxState `from `@react-stately/combobox` implements a JSX based interface for building collections instead. See Collection Components for more information, and Collection Interface for internal details. In addition, `useComboBoxState` manages the state necessary for single selection and exposes a` SelectionManager `, which makes use of the collection to provide an interface to update the selection state. It also holds state to track if the popup is open, if the combo box is focused, and the current input value. For more information about selection, see Selection. ## Example# * * * This example uses an `<input>` element for the combo box text input and a `<button>` element for the list box popup trigger. A `<span>` is included within the `<button>` to display the dropdown arrow icon (hidden from screen readers with `aria-hidden`). A "contains" filter function is obtained from `useFilter` and is passed to` useComboBoxState `so that the list box can be filtered based on the option text and the current input text. The same `Popover`, `ListBox`, and `Button` components created with usePopover, useListBox, and useButton that you may already have in your component library or application should be reused. These can be shared with other components such as a `Select` created with useSelect or a `Dialog` popover created with useDialog. The code for these components is also included below in the collapsed sections. In addition, see useListBox for examples of sections (option groups), and more complex options. For an example of the description and error message elements, see useTextField. import {useButton, useComboBox, useFilter} from 'react-aria'; import {Item, useComboBoxState} from 'react-stately'; // Reuse the ListBox, Popover, and Button from your component library. See below for details. import {Button, ListBox, Popover} from 'your-component-library'; function ComboBox(props) { // Setup filter function and state. let { contains } = useFilter({ sensitivity: 'base' }); let state = useComboBoxState({ ...props, defaultFilter: contains }); // Setup refs and get props for child elements. let buttonRef = React.useRef(null); let inputRef = React.useRef(null); let listBoxRef = React.useRef(null); let popoverRef = React.useRef(null); let { buttonProps, inputProps, listBoxProps, labelProps } = useComboBox( { ...props, inputRef, buttonRef, listBoxRef, popoverRef }, state ); return ( <div style={{ display: 'inline-flex', flexDirection: 'column' }}> <label {...labelProps}>{props.label}</label> <div> <input {...inputProps} ref={inputRef} style={{ height: 24, boxSizing: 'border-box', marginRight: 0, fontSize: 16 }} /> <Button {...buttonProps} buttonRef={buttonRef} style={{ height: 24, marginLeft: 0 }} > <span aria-hidden="true" style={{ padding: '0 2px' }} > ▼ </span> </Button> {state.isOpen && ( <Popover state={state} triggerRef={inputRef} popoverRef={popoverRef} isNonModal placement="bottom start" > <ListBox {...listBoxProps} listBoxRef={listBoxRef} state={state} /> </Popover> )} </div> </div> ); } <ComboBox label="Favorite Animal"> <Item key="red panda">Red Panda</Item> <Item key="cat">Cat</Item> <Item key="dog">Dog</Item> <Item key="aardvark">Aardvark</Item> <Item key="kangaroo">Kangaroo</Item> <Item key="snake">Snake</Item> </ComboBox> import { useButton, useComboBox, useFilter } from 'react-aria'; import {Item, useComboBoxState} from 'react-stately'; // Reuse the ListBox, Popover, and Button from your component library. See below for details. import { Button, ListBox, Popover } from 'your-component-library'; function ComboBox(props) { // Setup filter function and state. let { contains } = useFilter({ sensitivity: 'base' }); let state = useComboBoxState({ ...props, defaultFilter: contains }); // Setup refs and get props for child elements. let buttonRef = React.useRef(null); let inputRef = React.useRef(null); let listBoxRef = React.useRef(null); let popoverRef = React.useRef(null); let { buttonProps, inputProps, listBoxProps, labelProps } = useComboBox( { ...props, inputRef, buttonRef, listBoxRef, popoverRef }, state ); return ( <div style={{ display: 'inline-flex', flexDirection: 'column' }} > <label {...labelProps}>{props.label}</label> <div> <input {...inputProps} ref={inputRef} style={{ height: 24, boxSizing: 'border-box', marginRight: 0, fontSize: 16 }} /> <Button {...buttonProps} buttonRef={buttonRef} style={{ height: 24, marginLeft: 0 }} > <span aria-hidden="true" style={{ padding: '0 2px' }} > ▼ </span> </Button> {state.isOpen && ( <Popover state={state} triggerRef={inputRef} popoverRef={popoverRef} isNonModal placement="bottom start" > <ListBox {...listBoxProps} listBoxRef={listBoxRef} state={state} /> </Popover> )} </div> </div> ); } <ComboBox label="Favorite Animal"> <Item key="red panda">Red Panda</Item> <Item key="cat">Cat</Item> <Item key="dog">Dog</Item> <Item key="aardvark">Aardvark</Item> <Item key="kangaroo">Kangaroo</Item> <Item key="snake">Snake</Item> </ComboBox> import { useButton, useComboBox, useFilter } from 'react-aria'; import { Item, useComboBoxState } from 'react-stately'; // Reuse the ListBox, Popover, and Button from your component library. See below for details. import { Button, ListBox, Popover } from 'your-component-library'; function ComboBox( props ) { // Setup filter function and state. let { contains } = useFilter({ sensitivity: 'base' }); let state = useComboBoxState({ ...props, defaultFilter: contains }); // Setup refs and get props for child elements. let buttonRef = React .useRef(null); let inputRef = React .useRef(null); let listBoxRef = React .useRef(null); let popoverRef = React .useRef(null); let { buttonProps, inputProps, listBoxProps, labelProps } = useComboBox( { ...props, inputRef, buttonRef, listBoxRef, popoverRef }, state ); return ( <div style={{ display: 'inline-flex', flexDirection: 'column' }} > <label {...labelProps} > {props.label} </label> <div> <input {...inputProps} ref={inputRef} style={{ height: 24, boxSizing: 'border-box', marginRight: 0, fontSize: 16 }} /> <Button {...buttonProps} buttonRef={buttonRef} style={{ height: 24, marginLeft: 0 }} > <span aria-hidden="true" style={{ padding: '0 2px' }} > ▼ </span> </Button> {state.isOpen && ( <Popover state={state} triggerRef={inputRef} popoverRef={popoverRef} isNonModal placement="bottom start" > <ListBox {...listBoxProps} listBoxRef={listBoxRef} state={state} /> </Popover> )} </div> </div> ); } <ComboBox label="Favorite Animal"> <Item key="red panda"> Red Panda </Item> <Item key="cat"> Cat </Item> <Item key="dog"> Dog </Item> <Item key="aardvark"> Aardvark </Item> <Item key="kangaroo"> Kangaroo </Item> <Item key="snake"> Snake </Item> </ComboBox> Favorite Animal ▼ ### Popover# The `Popover` component is used to contain the popup listbox for the ComboBox. It can be shared between many other components, including Select, Menu, and others. See usePopover for more examples of popovers. Show code import {DismissButton, Overlay, usePopover} from 'react-aria'; import type {AriaPopoverProps} from 'react-aria'; import type {OverlayTriggerState} from 'react-stately'; interface PopoverProps extends AriaPopoverProps { children: React.ReactNode; state: OverlayTriggerState; } function Popover({ children, state, ...props }: PopoverProps) { let { popoverProps } = usePopover(props, state); return ( <Overlay> <div {...popoverProps} ref={props.popoverRef as React.RefObject<HTMLDivElement>} style={{ ...popoverProps.style, background: 'lightgray', border: '1px solid gray' }} > {children} <DismissButton onDismiss={state.close} /> </div> </Overlay> ); } import { DismissButton, Overlay, usePopover } from 'react-aria'; import type {AriaPopoverProps} from 'react-aria'; import type {OverlayTriggerState} from 'react-stately'; interface PopoverProps extends AriaPopoverProps { children: React.ReactNode; state: OverlayTriggerState; } function Popover( { children, state, ...props }: PopoverProps ) { let { popoverProps } = usePopover(props, state); return ( <Overlay> <div {...popoverProps} ref={props.popoverRef as React.RefObject< HTMLDivElement >} style={{ ...popoverProps.style, background: 'lightgray', border: '1px solid gray' }} > {children} <DismissButton onDismiss={state.close} /> </div> </Overlay> ); } import { DismissButton, Overlay, usePopover } from 'react-aria'; import type {AriaPopoverProps} from 'react-aria'; import type {OverlayTriggerState} from 'react-stately'; interface PopoverProps extends AriaPopoverProps { children: React.ReactNode; state: OverlayTriggerState; } function Popover( { children, state, ...props }: PopoverProps ) { let { popoverProps } = usePopover( props, state ); return ( <Overlay> <div {...popoverProps} ref={props .popoverRef as React.RefObject< HTMLDivElement >} style={{ ...popoverProps .style, background: 'lightgray', border: '1px solid gray' }} > {children} <DismissButton onDismiss={state .close} /> </div> </Overlay> ); } ### ListBox# The `ListBox` and `Option` components are used to show the filtered list of options as the user types in the ComboBox. They can also be shared with other components like a Select. See useListBox for more examples, including sections and more complex items. Show code import {useListBox, useOption} from 'react-aria'; function ListBox(props) { let ref = React.useRef(null); let { listBoxRef = ref, state } = props; let { listBoxProps } = useListBox(props, state, listBoxRef); return ( <ul {...listBoxProps} ref={listBoxRef} style={{ margin: 0, padding: 0, listStyle: 'none', maxHeight: 150, overflow: 'auto', minWidth: 200 }} > {[...state.collection].map((item) => ( <Option key={item.key} item={item} state={state} /> ))} </ul> ); } function Option({ item, state }) { let ref = React.useRef(null); let { optionProps, isSelected, isFocused, isDisabled } = useOption( { key: item.key }, state, ref ); let backgroundColor; let color = 'black'; if (isSelected) { backgroundColor = 'blueviolet'; color = 'white'; } else if (isFocused) { backgroundColor = 'gray'; } else if (isDisabled) { backgroundColor = 'transparent'; color = 'gray'; } return ( <li {...optionProps} ref={ref} style={{ background: backgroundColor, color: color, padding: '2px 5px', outline: 'none', cursor: 'pointer' }} > {item.rendered} </li> ); } import {useListBox, useOption} from 'react-aria'; function ListBox(props) { let ref = React.useRef(null); let { listBoxRef = ref, state } = props; let { listBoxProps } = useListBox( props, state, listBoxRef ); return ( <ul {...listBoxProps} ref={listBoxRef} style={{ margin: 0, padding: 0, listStyle: 'none', maxHeight: 150, overflow: 'auto', minWidth: 200 }} > {[...state.collection].map((item) => ( <Option key={item.key} item={item} state={state} /> ))} </ul> ); } function Option({ item, state }) { let ref = React.useRef(null); let { optionProps, isSelected, isFocused, isDisabled } = useOption({ key: item.key }, state, ref); let backgroundColor; let color = 'black'; if (isSelected) { backgroundColor = 'blueviolet'; color = 'white'; } else if (isFocused) { backgroundColor = 'gray'; } else if (isDisabled) { backgroundColor = 'transparent'; color = 'gray'; } return ( <li {...optionProps} ref={ref} style={{ background: backgroundColor, color: color, padding: '2px 5px', outline: 'none', cursor: 'pointer' }} > {item.rendered} </li> ); } import { useListBox, useOption } from 'react-aria'; function ListBox(props) { let ref = React.useRef( null ); let { listBoxRef = ref, state } = props; let { listBoxProps } = useListBox( props, state, listBoxRef ); return ( <ul {...listBoxProps} ref={listBoxRef} style={{ margin: 0, padding: 0, listStyle: 'none', maxHeight: 150, overflow: 'auto', minWidth: 200 }} > {[ ...state .collection ].map((item) => ( <Option key={item.key} item={item} state={state} /> ))} </ul> ); } function Option( { item, state } ) { let ref = React.useRef( null ); let { optionProps, isSelected, isFocused, isDisabled } = useOption( { key: item.key }, state, ref ); let backgroundColor; let color = 'black'; if (isSelected) { backgroundColor = 'blueviolet'; color = 'white'; } else if (isFocused) { backgroundColor = 'gray'; } else if ( isDisabled ) { backgroundColor = 'transparent'; color = 'gray'; } return ( <li {...optionProps} ref={ref} style={{ background: backgroundColor, color: color, padding: '2px 5px', outline: 'none', cursor: 'pointer' }} > {item.rendered} </li> ); } ### Button# The `Button` component is used in the above example to toggle the listbox popup. It is built using the useButton hook, and can be shared with many other components. Show code import {useButton} from 'react-aria'; function Button(props) { let ref = props.buttonRef; let { buttonProps } = useButton(props, ref); return ( <button {...buttonProps} ref={ref} style={props.style}> {props.children} </button> ); } import {useButton} from 'react-aria'; function Button(props) { let ref = props.buttonRef; let { buttonProps } = useButton(props, ref); return ( <button {...buttonProps} ref={ref} style={props.style}> {props.children} </button> ); } import {useButton} from 'react-aria'; function Button(props) { let ref = props.buttonRef; let { buttonProps } = useButton( props, ref ); return ( <button {...buttonProps} ref={ref} style={props.style} > {props.children} </button> ); } ## Styled examples# * * * Tailwind CSS An example of styling a ComboBox with Tailwind. Search autocomplete A search autocomplete with multiple sections, styled with Tailwind. Styled Components A ComboBox with complex item content built with Styled Components. Material UI An example ComboBox built with Material UI and React Aria. Chakra UI An async loading and infinite scrolling autocomplete built with Chakra UI. ## Usage# * * * The following examples show how to use the ComboBox component created in the above example. ### Uncontrolled# The following example shows how you would create an uncontrolled ComboBox. The input value, selected option, and open state is completely uncontrolled, with the default input text set by the `defaultInputValue` prop. <ComboBox label="Favorite Animal" defaultInputValue="red"> <Item key="red panda">Red Panda</Item> <Item key="cat">Cat</Item> <Item key="dog">Dog</Item> <Item key="aardvark">Aardvark</Item> <Item key="kangaroo">Kangaroo</Item> <Item key="snake">Snake</Item> </ComboBox> <ComboBox label="Favorite Animal" defaultInputValue="red"> <Item key="red panda">Red Panda</Item> <Item key="cat">Cat</Item> <Item key="dog">Dog</Item> <Item key="aardvark">Aardvark</Item> <Item key="kangaroo">Kangaroo</Item> <Item key="snake">Snake</Item> </ComboBox> <ComboBox label="Favorite Animal" defaultInputValue="red" > <Item key="red panda"> Red Panda </Item> <Item key="cat"> Cat </Item> <Item key="dog"> Dog </Item> <Item key="aardvark"> Aardvark </Item> <Item key="kangaroo"> Kangaroo </Item> <Item key="snake"> Snake </Item> </ComboBox> Favorite Animal ▼ ### Dynamic collections# ComboBox follows the Collection Components API, accepting both static and dynamic collections. The examples above show static collections, which can be used when the full list of options is known ahead of time. Dynamic collections, as shown below, can be used when the options come from an external data source such as an API call, or update over time. As seen below, an iterable list of options is passed to the ComboBox using the `defaultItems` prop. Each item accepts a `key` prop, which is passed to the `onSelectionChange` handler to identify the selected item. Alternatively, if the item objects contain an `id` property, as shown in the example below, then this is used automatically and a `key` prop is not required. function Example() { let options = [ {id: 1, name: 'Aerospace'}, {id: 2, name: 'Mechanical'}, {id: 3, name: 'Civil'}, {id: 4, name: 'Biomedical'}, {id: 5, name: 'Nuclear'}, {id: 6, name: 'Industrial'}, {id: 7, name: 'Chemical'}, {id: 8, name: 'Agricultural'}, {id: 9, name: 'Electrical'} ]; let [majorId, setMajorId] = React.useState(null); return ( <> <ComboBox label="Pick a engineering major" defaultItems={options} onSelectionChange={setMajorId}> {(item) => <Item>{item.name}</Item>} </ComboBox> <p>Selected topic id: {majorId}</p> </> ); } function Example() { let options = [ {id: 1, name: 'Aerospace'}, {id: 2, name: 'Mechanical'}, {id: 3, name: 'Civil'}, {id: 4, name: 'Biomedical'}, {id: 5, name: 'Nuclear'}, {id: 6, name: 'Industrial'}, {id: 7, name: 'Chemical'}, {id: 8, name: 'Agricultural'}, {id: 9, name: 'Electrical'} ]; let [majorId, setMajorId] = React.useState(null); return ( <> <ComboBox label="Pick a engineering major" defaultItems={options} onSelectionChange={setMajorId}> {(item) => <Item>{item.name}</Item>} </ComboBox> <p>Selected topic id: {majorId}</p> </> ); } function Example() { let options = [ { id: 1, name: 'Aerospace' }, { id: 2, name: 'Mechanical' }, { id: 3, name: 'Civil' }, { id: 4, name: 'Biomedical' }, { id: 5, name: 'Nuclear' }, { id: 6, name: 'Industrial' }, { id: 7, name: 'Chemical' }, { id: 8, name: 'Agricultural' }, { id: 9, name: 'Electrical' } ]; let [ majorId, setMajorId ] = React.useState( null ); return ( <> <ComboBox label="Pick a engineering major" defaultItems={options} onSelectionChange={setMajorId} > {(item) => ( <Item> {item.name} </Item> )} </ComboBox> <p> Selected topic id: {majorId} </p> </> ); } Pick a engineering major ▼ Selected topic id: ### Allow custom values# By default, `useComboBoxState` doesn't allow users to specify a value that doesn't exist in the list of options and will revert the input value to the current selected value on blur. By specifying `allowsCustomValue`, this behavior is suppressed and the user is free to enter any value within the field. <ComboBox label="Favorite Animal" allowsCustomValue> <Item key="red panda">Red Panda</Item> <Item key="cat">Cat</Item> <Item key="dog">Dog</Item> <Item key="aardvark">Aardvark</Item> <Item key="kangaroo">Kangaroo</Item> <Item key="snake">Snake</Item> </ComboBox> <ComboBox label="Favorite Animal" allowsCustomValue> <Item key="red panda">Red Panda</Item> <Item key="cat">Cat</Item> <Item key="dog">Dog</Item> <Item key="aardvark">Aardvark</Item> <Item key="kangaroo">Kangaroo</Item> <Item key="snake">Snake</Item> </ComboBox> <ComboBox label="Favorite Animal" allowsCustomValue > <Item key="red panda"> Red Panda </Item> <Item key="cat"> Cat </Item> <Item key="dog"> Dog </Item> <Item key="aardvark"> Aardvark </Item> <Item key="kangaroo"> Kangaroo </Item> <Item key="snake"> Snake </Item> </ComboBox> Favorite Animal ▼ ### Custom filtering# By default, `useComboBoxState` uses the filter function passed to the `defaultFilter` prop (in the above example, a "contains" function from `useFilter`). The filter function can be overridden by users of the `ComboBox` component by using the `items` prop to control the filtered list. When `items` is provided rather than `defaultItems`, `useComboBoxState` does no filtering of its own. The following example makes the `inputValue` controlled, and updates the filtered list that is passed to the `items` prop when the input changes value. function Example() { let options = [ {id: 1, email: 'fake@email.com'}, {id: 2, email: 'anotherfake@email.com'}, {id: 3, email: 'bob@email.com'}, {id: 4, email: 'joe@email.com'}, {id: 5, email: 'yourEmail@email.com'}, {id: 6, email: 'valid@email.com'}, {id: 7, email: 'spam@email.com'}, {id: 8, email: 'newsletter@email.com'}, {id: 9, email: 'subscribe@email.com'} ]; let {startsWith} = useFilter({sensitivity: 'base'}); let [filterValue, setFilterValue] = React.useState(''); let filteredItems = React.useMemo( () => options.filter((item) => startsWith(item.email, filterValue)), [options, filterValue] ); return ( <ComboBox label="To:" items={filteredItems} inputValue={filterValue} onInputChange={setFilterValue} allowsCustomValue> {(item) => <Item>{item.email}</Item>} </ComboBox> ); } function Example() { let options = [ { id: 1, email: 'fake@email.com' }, { id: 2, email: 'anotherfake@email.com' }, { id: 3, email: 'bob@email.com' }, { id: 4, email: 'joe@email.com' }, { id: 5, email: 'yourEmail@email.com' }, { id: 6, email: 'valid@email.com' }, { id: 7, email: 'spam@email.com' }, { id: 8, email: 'newsletter@email.com' }, { id: 9, email: 'subscribe@email.com' } ]; let { startsWith } = useFilter({ sensitivity: 'base' }); let [filterValue, setFilterValue] = React.useState(''); let filteredItems = React.useMemo( () => options.filter((item) => startsWith(item.email, filterValue) ), [options, filterValue] ); return ( <ComboBox label="To:" items={filteredItems} inputValue={filterValue} onInputChange={setFilterValue} allowsCustomValue > {(item) => <Item>{item.email}</Item>} </ComboBox> ); } function Example() { let options = [ { id: 1, email: 'fake@email.com' }, { id: 2, email: 'anotherfake@email.com' }, { id: 3, email: 'bob@email.com' }, { id: 4, email: 'joe@email.com' }, { id: 5, email: 'yourEmail@email.com' }, { id: 6, email: 'valid@email.com' }, { id: 7, email: 'spam@email.com' }, { id: 8, email: 'newsletter@email.com' }, { id: 9, email: 'subscribe@email.com' } ]; let { startsWith } = useFilter({ sensitivity: 'base' }); let [ filterValue, setFilterValue ] = React.useState(''); let filteredItems = React.useMemo( () => options.filter(( item ) => startsWith( item.email, filterValue ) ), [ options, filterValue ] ); return ( <ComboBox label="To:" items={filteredItems} inputValue={filterValue} onInputChange={setFilterValue} allowsCustomValue > {(item) => ( <Item> {item.email} </Item> )} </ComboBox> ); } To: ▼ ### Fully controlled# The following example shows how you would create a controlled ComboBox, controlling everything from the selected value (`selectedKey`) to the combobox options (`items`). By passing in `inputValue`, `selectedKey`, and `items` to the `ComboBox` you can control exactly what your ComboBox should display. For example, note that the item filtering for the controlled ComboBox below now follows a "starts with" filter strategy, accomplished by controlling the exact set of items available to the ComboBox whenever the input value updates. It is important to note that you don't have to control every single aspect of a ComboBox. If you decide to only control a single property of the ComboBox, be sure to provide the change handler for that prop as well e.g. controlling `selectedKey` would require `onSelectionChange` to be passed to `useComboBox` as well. function ControlledComboBox() { let optionList = [ { name: 'Red Panda', id: '1' }, { name: 'Cat', id: '2' }, { name: 'Dog', id: '3' }, { name: 'Aardvark', id: '4' }, { name: 'Kangaroo', id: '5' }, { name: 'Snake', id: '6' } ]; // Store ComboBox input value, selected option, open state, and items // in a state tracker let [fieldState, setFieldState] = React.useState({ selectedKey: '', inputValue: '', items: optionList }); // Implement custom filtering logic and control what items are // available to the ComboBox. let { startsWith } = useFilter({ sensitivity: 'base' }); // Specify how each of the ComboBox values should change when an // option is selected from the list box let onSelectionChange = (key) => { setFieldState((prevState) => { let selectedItem = prevState.items.find((option) => option.id === key); return ({ inputValue: selectedItem?.name ?? '', selectedKey: key, items: optionList.filter((item) => startsWith(item.name, selectedItem?.name ?? '') ) }); }); }; // Specify how each of the ComboBox values should change when the input // field is altered by the user let onInputChange = (value) => { setFieldState((prevState) => ({ inputValue: value, selectedKey: value === '' ? null : prevState.selectedKey, items: optionList.filter((item) => startsWith(item.name, value)) })); }; // Show entire list if user opens the menu manually let onOpenChange = (isOpen, menuTrigger) => { if (menuTrigger === 'manual' && isOpen) { setFieldState((prevState) => ({ inputValue: prevState.inputValue, selectedKey: prevState.selectedKey, items: optionList })); } }; // Pass each controlled prop to useComboBox along with their // change handlers return ( <ComboBox label="Favorite Animal" items={fieldState.items} selectedKey={fieldState.selectedKey} inputValue={fieldState.inputValue} onOpenChange={onOpenChange} onSelectionChange={onSelectionChange} onInputChange={onInputChange} > {(item) => <Item>{item.name}</Item>} </ComboBox> ); } <ControlledComboBox /> function ControlledComboBox() { let optionList = [ { name: 'Red Panda', id: '1' }, { name: 'Cat', id: '2' }, { name: 'Dog', id: '3' }, { name: 'Aardvark', id: '4' }, { name: 'Kangaroo', id: '5' }, { name: 'Snake', id: '6' } ]; // Store ComboBox input value, selected option, open state, and items // in a state tracker let [fieldState, setFieldState] = React.useState({ selectedKey: '', inputValue: '', items: optionList }); // Implement custom filtering logic and control what items are // available to the ComboBox. let { startsWith } = useFilter({ sensitivity: 'base' }); // Specify how each of the ComboBox values should change when an // option is selected from the list box let onSelectionChange = (key) => { setFieldState((prevState) => { let selectedItem = prevState.items.find((option) => option.id === key ); return ({ inputValue: selectedItem?.name ?? '', selectedKey: key, items: optionList.filter((item) => startsWith(item.name, selectedItem?.name ?? '') ) }); }); }; // Specify how each of the ComboBox values should change when the input // field is altered by the user let onInputChange = (value) => { setFieldState((prevState) => ({ inputValue: value, selectedKey: value === '' ? null : prevState.selectedKey, items: optionList.filter((item) => startsWith(item.name, value) ) })); }; // Show entire list if user opens the menu manually let onOpenChange = (isOpen, menuTrigger) => { if (menuTrigger === 'manual' && isOpen) { setFieldState((prevState) => ({ inputValue: prevState.inputValue, selectedKey: prevState.selectedKey, items: optionList })); } }; // Pass each controlled prop to useComboBox along with their // change handlers return ( <ComboBox label="Favorite Animal" items={fieldState.items} selectedKey={fieldState.selectedKey} inputValue={fieldState.inputValue} onOpenChange={onOpenChange} onSelectionChange={onSelectionChange} onInputChange={onInputChange} > {(item) => <Item>{item.name}</Item>} </ComboBox> ); } <ControlledComboBox /> function ControlledComboBox() { let optionList = [ { name: 'Red Panda', id: '1' }, { name: 'Cat', id: '2' }, { name: 'Dog', id: '3' }, { name: 'Aardvark', id: '4' }, { name: 'Kangaroo', id: '5' }, { name: 'Snake', id: '6' } ]; // Store ComboBox input value, selected option, open state, and items // in a state tracker let [ fieldState, setFieldState ] = React.useState({ selectedKey: '', inputValue: '', items: optionList }); // Implement custom filtering logic and control what items are // available to the ComboBox. let { startsWith } = useFilter({ sensitivity: 'base' }); // Specify how each of the ComboBox values should change when an // option is selected from the list box let onSelectionChange = (key) => { setFieldState( (prevState) => { let selectedItem = prevState .items .find( (option) => option .id === key ); return ({ inputValue: selectedItem ?.name ?? '', selectedKey: key, items: optionList .filter( (item) => startsWith( item .name, selectedItem ?.name ?? '' ) ) }); } ); }; // Specify how each of the ComboBox values should change when the input // field is altered by the user let onInputChange = ( value ) => { setFieldState( (prevState) => ({ inputValue: value, selectedKey: value === '' ? null : prevState .selectedKey, items: optionList .filter( (item) => startsWith( item .name, value ) ) }) ); }; // Show entire list if user opens the menu manually let onOpenChange = ( isOpen, menuTrigger ) => { if ( menuTrigger === 'manual' && isOpen ) { setFieldState( (prevState) => ({ inputValue: prevState .inputValue, selectedKey: prevState .selectedKey, items: optionList }) ); } }; // Pass each controlled prop to useComboBox along with their // change handlers return ( <ComboBox label="Favorite Animal" items={fieldState .items} selectedKey={fieldState .selectedKey} inputValue={fieldState .inputValue} onOpenChange={onOpenChange} onSelectionChange={onSelectionChange} onInputChange={onInputChange} > {(item) => ( <Item> {item.name} </Item> )} </ComboBox> ); } <ControlledComboBox /> Favorite Animal ▼ ### Menu trigger behavior# `useComboBoxState` supports three different `menuTrigger` prop values: * `input` (default): ComboBox menu opens when the user edits the input text. * `focus`: ComboBox menu opens when the user focuses the ComboBox input. * `manual`: ComboBox menu only opens when the user presses the trigger button or uses the arrow keys. The example below has `menuTrigger` set to `focus`. <ComboBox label="Favorite Animal" menuTrigger="focus"> <Item key="red panda">Red Panda</Item> <Item key="cat">Cat</Item> <Item key="dog">Dog</Item> <Item key="aardvark">Aardvark</Item> <Item key="kangaroo">Kangaroo</Item> <Item key="snake">Snake</Item> </ComboBox> <ComboBox label="Favorite Animal" menuTrigger="focus"> <Item key="red panda">Red Panda</Item> <Item key="cat">Cat</Item> <Item key="dog">Dog</Item> <Item key="aardvark">Aardvark</Item> <Item key="kangaroo">Kangaroo</Item> <Item key="snake">Snake</Item> </ComboBox> <ComboBox label="Favorite Animal" menuTrigger="focus" > <Item key="red panda"> Red Panda </Item> <Item key="cat"> Cat </Item> <Item key="dog"> Dog </Item> <Item key="aardvark"> Aardvark </Item> <Item key="kangaroo"> Kangaroo </Item> <Item key="snake"> Snake </Item> </ComboBox> Favorite Animal ▼ ### Disabled options# You can disable specific options by providing an array of keys to `useComboBoxState` via the `disabledKeys` prop. This will prevent options with matching keys from being pressable and receiving keyboard focus as shown in the example below. Note that you are responsible for the styling of disabled options. <ComboBox label="Favorite Animal" disabledKeys={['cat', 'kangaroo']}> <Item key="red panda">Red Panda</Item> <Item key="cat">Cat</Item> <Item key="dog">Dog</Item> <Item key="aardvark">Aardvark</Item> <Item key="kangaroo">Kangaroo</Item> <Item key="snake">Snake</Item> </ComboBox> <ComboBox label="Favorite Animal" disabledKeys={['cat', 'kangaroo']} > <Item key="red panda">Red Panda</Item> <Item key="cat">Cat</Item> <Item key="dog">Dog</Item> <Item key="aardvark">Aardvark</Item> <Item key="kangaroo">Kangaroo</Item> <Item key="snake">Snake</Item> </ComboBox> <ComboBox label="Favorite Animal" disabledKeys={[ 'cat', 'kangaroo' ]} > <Item key="red panda"> Red Panda </Item> <Item key="cat"> Cat </Item> <Item key="dog"> Dog </Item> <Item key="aardvark"> Aardvark </Item> <Item key="kangaroo"> Kangaroo </Item> <Item key="snake"> Snake </Item> </ComboBox> Favorite Animal ▼ ### Asynchronous loading# This example uses the useAsyncList hook to handle asynchronous loading and filtering of data from a server. You may additionally want to display a spinner to indicate the loading state to the user, or support features like infinite scroll to load more data. See this CodeSandbox for an example of a ComboBox supporting those features. import {useAsyncList} from 'react-stately'; function AsyncLoadingExample() { let list = useAsyncList({ async load({ signal, filterText }) { let res = await fetch( `https://swapi.py4e.com/api/people/?search=${filterText}`, { signal } ); let json = await res.json(); return { items: json.results }; } }); return ( <ComboBox label="Star Wars Character Lookup" items={list.items} inputValue={list.filterText} onInputChange={list.setFilterText} > {(item) => <Item key={item.name}>{item.name}</Item>} </ComboBox> ); } import {useAsyncList} from 'react-stately'; function AsyncLoadingExample() { let list = useAsyncList({ async load({ signal, filterText }) { let res = await fetch( `https://swapi.py4e.com/api/people/?search=${filterText}`, { signal } ); let json = await res.json(); return { items: json.results }; } }); return ( <ComboBox label="Star Wars Character Lookup" items={list.items} inputValue={list.filterText} onInputChange={list.setFilterText} > {(item) => <Item key={item.name}>{item.name}</Item>} </ComboBox> ); } import {useAsyncList} from 'react-stately'; function AsyncLoadingExample() { let list = useAsyncList({ async load( { signal, filterText } ) { let res = await fetch( `https://swapi.py4e.com/api/people/?search=${filterText}`, { signal } ); let json = await res .json(); return { items: json.results }; } }); return ( <ComboBox label="Star Wars Character Lookup" items={list.items} inputValue={list .filterText} onInputChange={list .setFilterText} > {(item) => ( <Item key={item.name} > {item.name} </Item> )} </ComboBox> ); } Star Wars Character Lookup ▼ ### Links# By default, interacting with an item in a ComboBox selects it and updates the input value. Alternatively, items may be links to another page or website. This can be achieved by passing the `href` prop to the `<Item>` component. Interacting with link items navigates to the provided URL and does not update the selection or input value. See the links section in the `useListBox` docs for details on how to support this. ## Internationalization# * * * `useComboBox` handles some aspects of internationalization automatically. For example, the item focus, count, and selection VoiceOver announcements are localized. You are responsible for localizing all labels and option content that is passed into the combo box. ### RTL# In right-to-left languages, the ComboBox should be mirrored. The trigger button should be on the left, and the input element should be on the right. In addition, the content of ComboBox options should flip. Ensure that your CSS accounts for this. --- ## Page: https://react-spectrum.adobe.com/react-aria/useSelect.html # useSelect Provides the behavior and accessibility implementation for a select component. A select displays a collapsible list of options and allows a user to select one of them. <table><tbody><tr><th>install</th><td><code>yarn add react-aria</code></td></tr><tr><th>version</th><td>3.41.1</td></tr><tr><th>usage</th><td><code><span>import</span> {useSelect} <span>from</span> <span>'react-aria'</span></code></td></tr></tbody></table> View ARIA pattern W3C View repository GitHub View package NPM ## API# * * * `useSelect<T>( props: AriaSelectOptions <T>, state: SelectState <T>, ref: RefObject <HTMLElement | | null> ): SelectAria <T>` ## Features# * * * A select can be built using the <select> and <option> HTML elements, but this is not possible to style consistently cross browser, especially the options. `useSelect` helps achieve accessible select components that can be styled as needed without compromising on high quality interactions. * Exposed to assistive technology as a button with a `listbox` popup using ARIA (combined with useListBox) * Support for selecting a single option * Support for disabled options * Support for sections * Labeling support for accessibility * Support for native HTML constraint validation with customizable UI, custom validation functions, realtime validation, and server-side validation errors * Support for mouse, touch, and keyboard interactions * Tab stop focus management * Keyboard support for opening the listbox using the arrow keys, including automatically focusing the first or last item accordingly * Typeahead to allow selecting options by typing text, even without opening the listbox * Browser autofill integration via a hidden native `<select>` element * Mobile screen reader listbox dismissal support ## Anatomy# * * * A select consists of a label, a button which displays a selected value, and a listbox, displayed in a popup. Users can click, touch, or use the keyboard on the button to open the listbox popup. `useSelect` handles exposing the correct ARIA attributes for accessibility and handles the interactions for the select in its collapsed state. It should be combined with useListBox, which handles the implementation of the popup listbox. `useSelect` also supports optional description and error message elements, which can be used to provide more context about the field, and any validation messages. These are linked with the input via the `aria-describedby` attribute. `useSelect` returns props that you should spread onto the appropriate element: | Name | Type | Description | | --- | --- | --- | | `labelProps` | `DOMAttributes` | Props for the label element. | | `triggerProps` | ` AriaButtonProps ` | Props for the popup trigger element. | | `valueProps` | `DOMAttributes` | Props for the element representing the selected value. | | `menuProps` | ` AriaListBoxOptions <T>` | Props for the popup. | | `descriptionProps` | `DOMAttributes` | Props for the select's description element, if any. | | `errorMessageProps` | `DOMAttributes` | Props for the select's error message element, if any. | | `isInvalid` | `boolean` | Whether the input value is invalid. | | `validationErrors` | `string[]` | The current error messages for the input if it is invalid, otherwise an empty array. | | `validationDetails` | `ValidityState` | The native validation details for the input. | State is managed by the `useSelectState` hook from `@react-stately/select`. The state object should be passed as an option to `useSelect` If a select does not have a visible label, an `aria-label` or `aria-labelledby` prop must be passed instead to identify it to assistive technology. ## State management# * * * `useSelect` requires knowledge of the options in the select in order to handle keyboard navigation and other interactions. It does this using the `Collection` interface, which is a generic interface to access sequential unique keyed data. You can implement this interface yourself, e.g. by using a prop to pass a list of item objects, but` useSelectState `from `@react-stately/select` implements a JSX based interface for building collections instead. See Collection Components for more information, and Collection Interface for internal details. In addition, `useSelectState` manages the state necessary for multiple selection and exposes a` SelectionManager `, which makes use of the collection to provide an interface to update the selection state. It also holds state to track if the popup is open. For more information about selection, see Selection. ## Example# * * * This example uses a `<button>` element for the trigger, with a `<span>` inside to hold the value, and another for the dropdown arrow icon (hidden from screen readers with `aria-hidden`). A < `HiddenSelect` \> is used to render a hidden native `<select>`, which enables browser form autofill support. The same `Popover`, `ListBox`, and `Button` components created with usePopover, useListBox, and useButton that you may already have in your component library or application should be reused. These can be shared with other components such as a `ComboBox` created with useComboBox or a `Dialog` popover created with useDialog. The code for these components is also included below in the collapsed sections. In addition, see useListBox for examples of sections (option groups), and more complex options. For an example of the description and error message elements, see useTextField. import {Item, useSelectState} from 'react-stately'; import {HiddenSelect, useSelect} from 'react-aria'; // Reuse the ListBox, Popover, and Button from your component library. See below for details. import {Button, ListBox, Popover} from 'your-component-library'; function Select(props) { // Create state based on the incoming props let state = useSelectState(props); // Get props for child elements from useSelect let ref = React.useRef(null); let { labelProps, triggerProps, valueProps, menuProps } = useSelect(props, state, ref); return ( <div style={{ display: 'inline-block' }}> <div {...labelProps}>{props.label}</div> <HiddenSelect isDisabled={props.isDisabled} state={state} triggerRef={ref} label={props.label} name={props.name} /> <Button {...triggerProps} buttonRef={ref} style={{ height: 30, fontSize: 14 }} > <span {...valueProps}> {state.selectedItem ? state.selectedItem.rendered : 'Select an option'} </span> <span aria-hidden="true" style={{ paddingLeft: 5 }} > ▼ </span> </Button> {state.isOpen && ( <Popover state={state} triggerRef={ref} placement="bottom start"> <ListBox {...menuProps} state={state} /> </Popover> )} </div> ); } <Select label="Favorite Color"> <Item>Red</Item> <Item>Orange</Item> <Item>Yellow</Item> <Item>Green</Item> <Item>Blue</Item> <Item>Purple</Item> <Item>Black</Item> <Item>White</Item> <Item>Lime</Item> <Item>Fushsia</Item> </Select> import {Item, useSelectState} from 'react-stately'; import {HiddenSelect, useSelect} from 'react-aria'; // Reuse the ListBox, Popover, and Button from your component library. See below for details. import { Button, ListBox, Popover } from 'your-component-library'; function Select(props) { // Create state based on the incoming props let state = useSelectState(props); // Get props for child elements from useSelect let ref = React.useRef(null); let { labelProps, triggerProps, valueProps, menuProps } = useSelect(props, state, ref); return ( <div style={{ display: 'inline-block' }}> <div {...labelProps}>{props.label}</div> <HiddenSelect isDisabled={props.isDisabled} state={state} triggerRef={ref} label={props.label} name={props.name} /> <Button {...triggerProps} buttonRef={ref} style={{ height: 30, fontSize: 14 }} > <span {...valueProps}> {state.selectedItem ? state.selectedItem.rendered : 'Select an option'} </span> <span aria-hidden="true" style={{ paddingLeft: 5 }} > ▼ </span> </Button> {state.isOpen && ( <Popover state={state} triggerRef={ref} placement="bottom start" > <ListBox {...menuProps} state={state} /> </Popover> )} </div> ); } <Select label="Favorite Color"> <Item>Red</Item> <Item>Orange</Item> <Item>Yellow</Item> <Item>Green</Item> <Item>Blue</Item> <Item>Purple</Item> <Item>Black</Item> <Item>White</Item> <Item>Lime</Item> <Item>Fushsia</Item> </Select> import { Item, useSelectState } from 'react-stately'; import { HiddenSelect, useSelect } from 'react-aria'; // Reuse the ListBox, Popover, and Button from your component library. See below for details. import { Button, ListBox, Popover } from 'your-component-library'; function Select(props) { // Create state based on the incoming props let state = useSelectState( props ); // Get props for child elements from useSelect let ref = React.useRef( null ); let { labelProps, triggerProps, valueProps, menuProps } = useSelect( props, state, ref ); return ( <div style={{ display: 'inline-block' }} > <div {...labelProps} > {props.label} </div> <HiddenSelect isDisabled={props .isDisabled} state={state} triggerRef={ref} label={props .label} name={props.name} /> <Button {...triggerProps} buttonRef={ref} style={{ height: 30, fontSize: 14 }} > <span {...valueProps} > {state .selectedItem ? state .selectedItem .rendered : 'Select an option'} </span> <span aria-hidden="true" style={{ paddingLeft: 5 }} > ▼ </span> </Button> {state.isOpen && ( <Popover state={state} triggerRef={ref} placement="bottom start" > <ListBox {...menuProps} state={state} /> </Popover> )} </div> ); } <Select label="Favorite Color"> <Item>Red</Item> <Item>Orange</Item> <Item>Yellow</Item> <Item>Green</Item> <Item>Blue</Item> <Item>Purple</Item> <Item>Black</Item> <Item>White</Item> <Item>Lime</Item> <Item>Fushsia</Item> </Select> Favorite Color Favorite ColorRedOrangeYellowGreenBluePurpleBlackWhiteLimeFushsia Select an option▼ ### Popover# The `Popover` component is used to contain the popup listbox for the Select. It can be shared between many other components, including ComboBox, Menu, and others. See usePopover for more examples of popovers. Show code import {DismissButton, Overlay, usePopover} from 'react-aria'; import type {AriaPopoverProps} from 'react-aria'; import type {OverlayTriggerState} from 'react-stately'; interface PopoverProps extends Omit<AriaPopoverProps, 'popoverRef'> { children: React.ReactNode; state: OverlayTriggerState; } function Popover({ children, state, ...props }: PopoverProps) { let popoverRef = React.useRef(null); let { popoverProps, underlayProps } = usePopover({ ...props, popoverRef }, state); return ( <Overlay> <div {...underlayProps} style={{ position: 'fixed', inset: 0 }} /> <div {...popoverProps} ref={popoverRef} style={{ ...popoverProps.style, background: 'var(--page-background)', border: '1px solid gray' }} > <DismissButton onDismiss={state.close} /> {children} <DismissButton onDismiss={state.close} /> </div> </Overlay> ); } import { DismissButton, Overlay, usePopover } from 'react-aria'; import type {AriaPopoverProps} from 'react-aria'; import type {OverlayTriggerState} from 'react-stately'; interface PopoverProps extends Omit<AriaPopoverProps, 'popoverRef'> { children: React.ReactNode; state: OverlayTriggerState; } function Popover( { children, state, ...props }: PopoverProps ) { let popoverRef = React.useRef(null); let { popoverProps, underlayProps } = usePopover({ ...props, popoverRef }, state); return ( <Overlay> <div {...underlayProps} style={{ position: 'fixed', inset: 0 }} /> <div {...popoverProps} ref={popoverRef} style={{ ...popoverProps.style, background: 'var(--page-background)', border: '1px solid gray' }} > <DismissButton onDismiss={state.close} /> {children} <DismissButton onDismiss={state.close} /> </div> </Overlay> ); } import { DismissButton, Overlay, usePopover } from 'react-aria'; import type {AriaPopoverProps} from 'react-aria'; import type {OverlayTriggerState} from 'react-stately'; interface PopoverProps extends Omit< AriaPopoverProps, 'popoverRef' > { children: React.ReactNode; state: OverlayTriggerState; } function Popover( { children, state, ...props }: PopoverProps ) { let popoverRef = React .useRef(null); let { popoverProps, underlayProps } = usePopover({ ...props, popoverRef }, state); return ( <Overlay> <div {...underlayProps} style={{ position: 'fixed', inset: 0 }} /> <div {...popoverProps} ref={popoverRef} style={{ ...popoverProps .style, background: 'var(--page-background)', border: '1px solid gray' }} > <DismissButton onDismiss={state .close} /> {children} <DismissButton onDismiss={state .close} /> </div> </Overlay> ); } ### ListBox# The `ListBox` and `Option` components are used to show the list of options. They can also be shared with other components like a ComboBox. See useListBox for more examples, including sections and more complex items. Show code import {useListBox, useOption} from 'react-aria'; function ListBox(props) { let ref = React.useRef(null); let { listBoxRef = ref, state } = props; let { listBoxProps } = useListBox(props, state, listBoxRef); return ( <ul {...listBoxProps} ref={listBoxRef} style={{ margin: 0, padding: 0, listStyle: 'none', maxHeight: 150, overflow: 'auto', minWidth: 100, background: 'lightgray' }} > {[...state.collection].map((item) => ( <Option key={item.key} item={item} state={state} /> ))} </ul> ); } function Option({ item, state }) { let ref = React.useRef(null); let { optionProps, isSelected, isFocused, isDisabled } = useOption( { key: item.key }, state, ref ); return ( <li {...optionProps} ref={ref} style={{ background: isFocused ? 'gray' : 'transparent', color: isDisabled ? 'gray' : isFocused ? 'white' : 'black', padding: '2px 5px', outline: 'none', cursor: 'pointer', display: 'flex', justifyContent: 'space-between', gap: '10px' }} > {item.rendered} {isSelected ? <span>✓</span> : null} </li> ); } import {useListBox, useOption} from 'react-aria'; function ListBox(props) { let ref = React.useRef(null); let { listBoxRef = ref, state } = props; let { listBoxProps } = useListBox( props, state, listBoxRef ); return ( <ul {...listBoxProps} ref={listBoxRef} style={{ margin: 0, padding: 0, listStyle: 'none', maxHeight: 150, overflow: 'auto', minWidth: 100, background: 'lightgray' }} > {[...state.collection].map((item) => ( <Option key={item.key} item={item} state={state} /> ))} </ul> ); } function Option({ item, state }) { let ref = React.useRef(null); let { optionProps, isSelected, isFocused, isDisabled } = useOption({ key: item.key }, state, ref); return ( <li {...optionProps} ref={ref} style={{ background: isFocused ? 'gray' : 'transparent', color: isDisabled ? 'gray' : isFocused ? 'white' : 'black', padding: '2px 5px', outline: 'none', cursor: 'pointer', display: 'flex', justifyContent: 'space-between', gap: '10px' }} > {item.rendered} {isSelected ? <span>✓</span> : null} </li> ); } import { useListBox, useOption } from 'react-aria'; function ListBox(props) { let ref = React.useRef( null ); let { listBoxRef = ref, state } = props; let { listBoxProps } = useListBox( props, state, listBoxRef ); return ( <ul {...listBoxProps} ref={listBoxRef} style={{ margin: 0, padding: 0, listStyle: 'none', maxHeight: 150, overflow: 'auto', minWidth: 100, background: 'lightgray' }} > {[ ...state .collection ].map((item) => ( <Option key={item.key} item={item} state={state} /> ))} </ul> ); } function Option( { item, state } ) { let ref = React.useRef( null ); let { optionProps, isSelected, isFocused, isDisabled } = useOption( { key: item.key }, state, ref ); return ( <li {...optionProps} ref={ref} style={{ background: isFocused ? 'gray' : 'transparent', color: isDisabled ? 'gray' : isFocused ? 'white' : 'black', padding: '2px 5px', outline: 'none', cursor: 'pointer', display: 'flex', justifyContent: 'space-between', gap: '10px' }} > {item.rendered} {isSelected ? <span>✓</span> : null} </li> ); } ### Button# The `Button` component is used in the above example to toggle the listbox popup. It is built using the useButton hook, and can be shared with many other components. Show code import {useButton} from 'react-aria'; function Button(props) { let ref = props.buttonRef; let { buttonProps } = useButton(props, ref); return ( <button {...buttonProps} ref={ref} style={props.style}> {props.children} </button> ); } import {useButton} from 'react-aria'; function Button(props) { let ref = props.buttonRef; let { buttonProps } = useButton(props, ref); return ( <button {...buttonProps} ref={ref} style={props.style}> {props.children} </button> ); } import {useButton} from 'react-aria'; function Button(props) { let ref = props.buttonRef; let { buttonProps } = useButton( props, ref ); return ( <button {...buttonProps} ref={ref} style={props.style} > {props.children} </button> ); } ## Styled examples# * * * Tailwind CSS An example of styling a Select with Tailwind. Styled Components A Select with complex item content built with Styled Components. Popup positioning A Select with custom macOS-style popup positioning. ## Usage# * * * The following examples show how to use the Select component created in the above example. ### Dynamic collections# `Select` follows the Collection Components API, accepting both static and dynamic collections. The examples above show static collections, which can be used when the full list of options is known ahead of time. Dynamic collections, as shown below, can be used when the options come from an external data source such as an API call, or update over time. As seen below, an iterable list of options is passed to the Select using the `items` prop. Each item accepts a `key` prop, which is passed to the `onSelectionChange` handler to identify the selected item. Alternatively, if the item objects contain an `id` property, as shown in the example below, then this is used automatically and a `key` prop is not required. function Example() { let options = [ {id: 1, name: 'Aerospace'}, {id: 2, name: 'Mechanical'}, {id: 3, name: 'Civil'}, {id: 4, name: 'Biomedical'}, {id: 5, name: 'Nuclear'}, {id: 6, name: 'Industrial'}, {id: 7, name: 'Chemical'}, {id: 8, name: 'Agricultural'}, {id: 9, name: 'Electrical'} ]; return ( <> <Select label="Pick an engineering major" items={options}> {(item) => <Item>{item.name}</Item>} </Select> </> ); } function Example() { let options = [ { id: 1, name: 'Aerospace' }, { id: 2, name: 'Mechanical' }, { id: 3, name: 'Civil' }, { id: 4, name: 'Biomedical' }, { id: 5, name: 'Nuclear' }, { id: 6, name: 'Industrial' }, { id: 7, name: 'Chemical' }, { id: 8, name: 'Agricultural' }, { id: 9, name: 'Electrical' } ]; return ( <> <Select label="Pick an engineering major" items={options} > {(item) => <Item>{item.name}</Item>} </Select> </> ); } function Example() { let options = [ { id: 1, name: 'Aerospace' }, { id: 2, name: 'Mechanical' }, { id: 3, name: 'Civil' }, { id: 4, name: 'Biomedical' }, { id: 5, name: 'Nuclear' }, { id: 6, name: 'Industrial' }, { id: 7, name: 'Chemical' }, { id: 8, name: 'Agricultural' }, { id: 9, name: 'Electrical' } ]; return ( <> <Select label="Pick an engineering major" items={options} > {(item) => ( <Item> {item.name} </Item> )} </Select> </> ); } Pick an engineering major Pick an engineering majorAerospaceMechanicalCivilBiomedicalNuclearIndustrialChemicalAgriculturalElectrical Select an option▼ ### Controlled selection# Setting a selected option can be done by using the `defaultSelectedKey` or `selectedKey` prop. The selected key corresponds to the `key` of an item. When `Select` is used with a dynamic collection as described above, the key of each item is derived from the data. See the `react-stately` Selection docs for more details. function Example() { let options = [ {name: 'Koala'}, {name: 'Kangaroo'}, {name: 'Platypus'}, {name: 'Bald Eagle'}, {name: 'Bison'}, {name: 'Skunk'} ]; let [animal, setAnimal] = React.useState("Bison"); return ( <Select label="Pick an animal (controlled)" items={options} selectedKey={animal} onSelectionChange={selected => setAnimal(selected)}> {item => <Item key={item.name}>{item.name}</Item>} </Select> ); } function Example() { let options = [ {name: 'Koala'}, {name: 'Kangaroo'}, {name: 'Platypus'}, {name: 'Bald Eagle'}, {name: 'Bison'}, {name: 'Skunk'} ]; let [animal, setAnimal] = React.useState("Bison"); return ( <Select label="Pick an animal (controlled)" items={options} selectedKey={animal} onSelectionChange={selected => setAnimal(selected)}> {item => <Item key={item.name}>{item.name}</Item>} </Select> ); } function Example() { let options = [ { name: 'Koala' }, { name: 'Kangaroo' }, { name: 'Platypus' }, { name: 'Bald Eagle' }, { name: 'Bison' }, { name: 'Skunk' } ]; let [ animal, setAnimal ] = React.useState( 'Bison' ); return ( <Select label="Pick an animal (controlled)" items={options} selectedKey={animal} onSelectionChange={(selected) => setAnimal( selected )} > {(item) => ( <Item key={item.name} > {item.name} </Item> )} </Select> ); } Pick an animal (controlled) Pick an animal (controlled)KoalaKangarooPlatypusBald EagleBisonSkunk Bison▼ ### Asynchronous loading# This example uses the useAsyncList hook to handle asynchronous loading of data from a server. You may additionally want to display a spinner to indicate the loading state to the user, or support features like infinite scroll to load more data. import {useAsyncList} from 'react-stately'; function AsyncLoadingExample() { let list = useAsyncList({ async load({ signal, filterText }) { let res = await fetch( `https://pokeapi.co/api/v2/pokemon`, { signal } ); let json = await res.json(); return { items: json.results }; } }); return ( <Select label="Pick a Pokemon" items={list.items} selectionMode="single"> {(item) => <Item key={item.name}>{item.name}</Item>} </Select> ); } import {useAsyncList} from 'react-stately'; function AsyncLoadingExample() { let list = useAsyncList({ async load({ signal, filterText }) { let res = await fetch( `https://pokeapi.co/api/v2/pokemon`, { signal } ); let json = await res.json(); return { items: json.results }; } }); return ( <Select label="Pick a Pokemon" items={list.items} selectionMode="single" > {(item) => <Item key={item.name}>{item.name}</Item>} </Select> ); } import {useAsyncList} from 'react-stately'; function AsyncLoadingExample() { let list = useAsyncList({ async load( { signal, filterText } ) { let res = await fetch( `https://pokeapi.co/api/v2/pokemon`, { signal } ); let json = await res .json(); return { items: json.results }; } }); return ( <Select label="Pick a Pokemon" items={list.items} selectionMode="single" > {(item) => ( <Item key={item.name} > {item.name} </Item> )} </Select> ); } Pick a Pokemon Pick a Pokemonbulbasaurivysaurvenusaurcharmandercharmeleoncharizardsquirtlewartortleblastoisecaterpiemetapodbutterfreeweedlekakunabeedrillpidgeypidgeottopidgeotrattataraticate Select an option▼ ### Disabled# A Select can be fully disabled using the `isDisabled` prop. <Select label="Choose frequency" isDisabled> <Item key="rarely">Rarely</Item> <Item key="sometimes">Sometimes</Item> <Item key="always">Always</Item> </Select> <Select label="Choose frequency" isDisabled> <Item key="rarely">Rarely</Item> <Item key="sometimes">Sometimes</Item> <Item key="always">Always</Item> </Select> <Select label="Choose frequency" isDisabled > <Item key="rarely"> Rarely </Item> <Item key="sometimes"> Sometimes </Item> <Item key="always"> Always </Item> </Select> Choose frequency Choose frequencyRarelySometimesAlways Select an option▼ ### Disabled options# `useSelect` supports marking items as disabled using the `disabledKeys` prop. Each key in this list corresponds with the `key` prop passed to the `Item` component, or automatically derived from the values passed to the `items` prop. See Collections for more details. Disabled items are not focusable, selectable, or keyboard navigable. The `isDisabled` property returned by `useOption` can be used to style the item appropriately. <Select label="Favorite Animal" disabledKeys={['cat', 'kangaroo']}> <Item key="red panda">Red Panda</Item> <Item key="cat">Cat</Item> <Item key="dog">Dog</Item> <Item key="aardvark">Aardvark</Item> <Item key="kangaroo">Kangaroo</Item> <Item key="snake">Snake</Item> </Select> <Select label="Favorite Animal" disabledKeys={['cat', 'kangaroo']} > <Item key="red panda">Red Panda</Item> <Item key="cat">Cat</Item> <Item key="dog">Dog</Item> <Item key="aardvark">Aardvark</Item> <Item key="kangaroo">Kangaroo</Item> <Item key="snake">Snake</Item> </Select> <Select label="Favorite Animal" disabledKeys={[ 'cat', 'kangaroo' ]} > <Item key="red panda"> Red Panda </Item> <Item key="cat"> Cat </Item> <Item key="dog"> Dog </Item> <Item key="aardvark"> Aardvark </Item> <Item key="kangaroo"> Kangaroo </Item> <Item key="snake"> Snake </Item> </Select> Favorite Animal Favorite AnimalRed PandaCatDogAardvarkKangarooSnake Select an option▼ ### Controlled open state# The open state of the select can be controlled via the `defaultOpen` and `isOpen` props function Example() { let [open, setOpen] = React.useState(false); return ( <> <p>Select is {open ? 'open' : 'closed'}</p> <Select label="Choose frequency" isOpen={open} onOpenChange={setOpen}> <Item key="rarely">Rarely</Item> <Item key="sometimes">Sometimes</Item> <Item key="always">Always</Item> </Select> </> ); } function Example() { let [open, setOpen] = React.useState(false); return ( <> <p>Select is {open ? 'open' : 'closed'}</p> <Select label="Choose frequency" isOpen={open} onOpenChange={setOpen} > <Item key="rarely">Rarely</Item> <Item key="sometimes">Sometimes</Item> <Item key="always">Always</Item> </Select> </> ); } function Example() { let [open, setOpen] = React.useState( false ); return ( <> <p> Select is {open ? 'open' : 'closed'} </p> <Select label="Choose frequency" isOpen={open} onOpenChange={setOpen} > <Item key="rarely"> Rarely </Item> <Item key="sometimes"> Sometimes </Item> <Item key="always"> Always </Item> </Select> </> ); } Select is closed Choose frequency Choose frequencyRarelySometimesAlways Select an option▼ ### Links# By default, interacting with an item in a Select triggers `onSelectionChange`. Alternatively, items may be links to another page or website. This can be achieved by passing the `href` prop to the `<Item>` component. Link items in a `Select` are not selectable. See the links section in the `useListBox` docs for details on how to support this. ## Internationalization# * * * `useSelect` and `useListBox` handle some aspects of internationalization automatically. For example, type to select is implemented with an Intl.Collator for internationalized string matching. You are responsible for localizing all labels and option content that is passed into the select. ### RTL# In right-to-left languages, the select should be mirrored. The arrow should be on the left, and the selected value should be on the right. In addition, the content of list options should flip. Ensure that your CSS accounts for this. ## Accessibility# * * * ### False positives# The HiddenSelect may trigger a known accessibility false positive from automated accessibility testing tools. This is because the HiddenSelect is included to specifically aid with browser form autocomplete and is hidden from screen readers via `aria-hidden` since users don't need to interact with it. We manage focus internally so that focusing this hidden select element will always shift focus to the Select's trigger button instead. Automated accessibility testing tools have no way of knowing that we manage the focus in this way and thus throw this false positive. To facilitate the suppression of this false positive, the `data-a11y-ignore="aria-hidden-focus"` data attribute is automatically applied to the problematic element and references the relevant `AXE` rule. Please use this data attribute to target the problematic element and exclude it from your automated accessibility tests as shown here. --- ## Page: https://react-spectrum.adobe.com/react-aria/useMeter.html # useMeter Provides the accessibility implementation for a meter component. Meters represent a quantity within a known range, or a fractional value. <table><tbody><tr><th>install</th><td><code>yarn add react-aria</code></td></tr><tr><th>version</th><td>3.41.1</td></tr><tr><th>usage</th><td><code><span>import</span> {useMeter} <span>from</span> <span>'react-aria'</span></code></td></tr></tbody></table> View ARIA pattern W3C View repository GitHub View package NPM ## API# * * * `useMeter( (props: AriaMeterProps )): MeterAria ` ## Features# * * * The <meter> HTML element can be used to build a meter, however it is very difficult to style cross browser. `useMeter` helps achieve accessible meters that can be styled as needed. Meters are similar to progress bars, but represent a quantity as opposed to progress over time. See the useProgressBar hook for more details about progress bars. * Exposed to assistive technology as a `meter` via ARIA, with fallback to `progressbar` where unsupported * Labeling support for accessibility * Internationalized number formatting as a percentage or value ## Anatomy# * * * Meters consist of a track element showing the full value in a range, a fill element showing the current value, a label, and an optional value label. The track and bar elements represent the value visually, while a wrapper element represents the meter to assistive technology using the meter ARIA role. `useMeter` returns two sets of props that you should spread onto the appropriate element: | Name | Type | Description | | --- | --- | --- | | `meterProps` | `DOMAttributes` | Props for the meter container element. | | `labelProps` | `DOMAttributes` | Props for the meter's visual label (if any). | If there is no visual label, an `aria-label` or `aria-labelledby` prop must be passed instead to identify the element to screen readers. ## Example# * * * import {useMeter} from 'react-aria'; function Meter(props) { let { label, showValueLabel = !!label, value, minValue = 0, maxValue = 100 } = props; let { meterProps, labelProps } = useMeter(props); // Calculate the width of the progress bar as a percentage let percentage = (value - minValue) / (maxValue - minValue); let barWidth = `${Math.round(percentage * 100)}%`; return ( <div {...meterProps} style={{ width: 200 }}> <div style={{ display: 'flex', justifyContent: 'space-between' }}> {label && ( <span {...labelProps}> {label} </span> )} {showValueLabel && ( <span> {meterProps['aria-valuetext']} </span> )} </div> <div style={{ height: 10, background: 'lightgray' }}> <div style={{ width: barWidth, height: 10, background: 'green' }} /> </div> </div> ); } <Meter label="Storage space" value={25} /> import {useMeter} from 'react-aria'; function Meter(props) { let { label, showValueLabel = !!label, value, minValue = 0, maxValue = 100 } = props; let { meterProps, labelProps } = useMeter(props); // Calculate the width of the progress bar as a percentage let percentage = (value - minValue) / (maxValue - minValue); let barWidth = `${Math.round(percentage * 100)}%`; return ( <div {...meterProps} style={{ width: 200 }}> <div style={{ display: 'flex', justifyContent: 'space-between' }} > {label && ( <span {...labelProps}> {label} </span> )} {showValueLabel && ( <span> {meterProps['aria-valuetext']} </span> )} </div> <div style={{ height: 10, background: 'lightgray' }}> <div style={{ width: barWidth, height: 10, background: 'green' }} /> </div> </div> ); } <Meter label="Storage space" value={25} /> import {useMeter} from 'react-aria'; function Meter(props) { let { label, showValueLabel = !!label, value, minValue = 0, maxValue = 100 } = props; let { meterProps, labelProps } = useMeter(props); // Calculate the width of the progress bar as a percentage let percentage = (value - minValue) / (maxValue - minValue); let barWidth = `${ Math.round( percentage * 100 ) }%`; return ( <div {...meterProps} style={{ width: 200 }} > <div style={{ display: 'flex', justifyContent: 'space-between' }} > {label && ( <span {...labelProps} > {label} </span> )} {showValueLabel && ( <span> {meterProps[ 'aria-valuetext' ]} </span> )} </div> <div style={{ height: 10, background: 'lightgray' }} > <div style={{ width: barWidth, height: 10, background: 'green' }} /> </div> </div> ); } <Meter label="Storage space" value={25} /> Storage space25% ## Styled examples# * * * Circular Gauge A circular meter built with SVG. ## Usage# * * * The following examples show how to use the `Meter` component created in the above example. ### Custom value scale# By default, the `value` prop represents the current percentage of progress, as the minimum and maximum values default to 0 and 100, respectively. Alternatively, a different scale can be used by setting the `minValue` and `maxValue` props. <Meter label="Widgets Used" minValue={50} maxValue={150} value={100} /> <Meter label="Widgets Used" minValue={50} maxValue={150} value={100} /> <Meter label="Widgets Used" minValue={50} maxValue={150} value={100} /> Widgets Used50% ### Value formatting# Values are formatted as a percentage by default, but this can be modified by using the `formatOptions` prop to specify a different format. `formatOptions` is compatible with the option parameter of Intl.NumberFormat and is applied based on the current locale. <Meter label="Currency" formatOptions={{style: 'currency', currency: 'JPY'}} value={60} /> <Meter label="Currency" formatOptions={{style: 'currency', currency: 'JPY'}} value={60} /> <Meter label="Currency" formatOptions={{ style: 'currency', currency: 'JPY' }} value={60} /> Currency¥60 ### Custom value label# The `valueLabel` prop allows the formatted value to be replaced with a custom string. <Meter label="Space used" valueLabel="54 of 60GB" value={90} /> <Meter label="Space used" valueLabel="54 of 60GB" value={90} /> <Meter label="Space used" valueLabel="54 of 60GB" value={90} /> Space used54 of 60GB ## Internationalization# * * * ### Value formatting# `useMeter` will handle localized formatting of the value label for accessibility automatically. This is returned in the `aria-valuetext` prop in `meterProps`. You can use this to create a visible label if needed and ensure that it is formatted correctly. The number formatting can also be controlled using the `formatOptions` prop. ### RTL# In right-to-left languages, the meter should be mirrored. The label is right-aligned, the value is left-aligned, and the fill progresses from right to left. Ensure that your CSS accounts for this. --- ## Page: https://react-spectrum.adobe.com/react-aria/useProgressBar.html # useProgressBar Provides the accessibility implementation for a progress bar component. Progress bars show either determinate or indeterminate progress of an operation over time. <table><tbody><tr><th>install</th><td><code>yarn add react-aria</code></td></tr><tr><th>version</th><td>3.41.1</td></tr><tr><th>usage</th><td><code><span>import</span> {useProgressBar} <span>from</span> <span>'react-aria'</span></code></td></tr></tbody></table> View ARIA pattern W3C View repository GitHub View package NPM ## API# * * * `useProgressBar( (props: AriaProgressBarProps )): ProgressBarAria ` ## Features# * * * The <progress> HTML element can be used to build a progress bar, however it is very difficult to style cross browser. `useProgressBar` helps achieve accessible progress bars and spinners that can be styled as needed. * Exposed to assistive technology as a progress bar via ARIA * Labeling support for accessibility * Internationalized number formatting as a percentage or value * Determinate and indeterminate progress support ## Anatomy# * * * Progress bars consist of a track element showing the full progress of an operation, a fill element showing the current progress, a label, and an optional value label. The track and bar elements represent the progress visually, while a wrapper element represents the progress to assistive technology using the progressbar ARIA role. `useProgressBar` returns two sets of props that you should spread onto the appropriate element: | Name | Type | Description | | --- | --- | --- | | `progressBarProps` | `DOMAttributes` | Props for the progress bar container element. | | `labelProps` | `DOMAttributes` | Props for the progress bar's visual label element (if any). | If there is no visual label, an `aria-label` or `aria-labelledby` prop must be passed instead to identify the element to screen readers. ## Example# * * * import {useProgressBar} from 'react-aria'; function ProgressBar(props) { let { label, showValueLabel = !!label, value, minValue = 0, maxValue = 100 } = props; let { progressBarProps, labelProps } = useProgressBar(props); // Calculate the width of the progress bar as a percentage let percentage = (value - minValue) / (maxValue - minValue); let barWidth = `${Math.round(percentage * 100)}%`; return ( <div {...progressBarProps} style={{ width: 200 }}> <div style={{ display: 'flex', justifyContent: 'space-between' }}> {label && ( <span {...labelProps}> {label} </span> )} {showValueLabel && ( <span> {progressBarProps['aria-valuetext']} </span> )} </div> <div style={{ height: 10, background: 'lightgray' }}> <div style={{ width: barWidth, height: 10, background: 'orange' }} /> </div> </div> ); } <ProgressBar label="Loading…" value={80} /> import {useProgressBar} from 'react-aria'; function ProgressBar(props) { let { label, showValueLabel = !!label, value, minValue = 0, maxValue = 100 } = props; let { progressBarProps, labelProps } = useProgressBar(props); // Calculate the width of the progress bar as a percentage let percentage = (value - minValue) / (maxValue - minValue); let barWidth = `${Math.round(percentage * 100)}%`; return ( <div {...progressBarProps} style={{ width: 200 }}> <div style={{ display: 'flex', justifyContent: 'space-between' }} > {label && ( <span {...labelProps}> {label} </span> )} {showValueLabel && ( <span> {progressBarProps['aria-valuetext']} </span> )} </div> <div style={{ height: 10, background: 'lightgray' }}> <div style={{ width: barWidth, height: 10, background: 'orange' }} /> </div> </div> ); } <ProgressBar label="Loading…" value={80} /> import {useProgressBar} from 'react-aria'; function ProgressBar( props ) { let { label, showValueLabel = !!label, value, minValue = 0, maxValue = 100 } = props; let { progressBarProps, labelProps } = useProgressBar( props ); // Calculate the width of the progress bar as a percentage let percentage = (value - minValue) / (maxValue - minValue); let barWidth = `${ Math.round( percentage * 100 ) }%`; return ( <div {...progressBarProps} style={{ width: 200 }} > <div style={{ display: 'flex', justifyContent: 'space-between' }} > {label && ( <span {...labelProps} > {label} </span> )} {showValueLabel && ( <span> {progressBarProps[ 'aria-valuetext' ]} </span> )} </div> <div style={{ height: 10, background: 'lightgray' }} > <div style={{ width: barWidth, height: 10, background: 'orange' }} /> </div> </div> ); } <ProgressBar label="Loading…" value={80} /> Loading…80% ## Circular example# * * * Progress bars may also be represented using a circular visualization rather than a line. This is often used to represent indeterminate operations, but may also be used for determinate progress indicators when space is limited. The following example shows a progress bar visualized as a circular spinner using SVG. function ProgressCircle(props) { let { isIndeterminate, value, minValue = 0, maxValue = 100 } = props; let { progressBarProps } = useProgressBar(props); let center = 16; let strokeWidth = 4; let r = 16 - strokeWidth; let c = 2 * r * Math.PI; let percentage = isIndeterminate ? 0.25 : (value - minValue) / (maxValue - minValue); let offset = c - percentage * c; return ( <svg {...progressBarProps} width={32} height={32} viewBox="0 0 32 32" fill="none" strokeWidth={strokeWidth} > <circle role="presentation" cx={center} cy={center} r={r} stroke="gray" /> <circle role="presentation" cx={center} cy={center} r={r} stroke="orange" strokeDasharray={`${c} ${c}`} strokeDashoffset={offset} transform="rotate(-90 16 16)" > {props.isIndeterminate && ( <animateTransform attributeName="transform" type="rotate" begin="0s" dur="1s" from="0 16 16" to="360 16 16" repeatCount="indefinite" /> )} </circle> </svg> ); } <ProgressCircle aria-label="Loading…" value={60} /> function ProgressCircle(props) { let { isIndeterminate, value, minValue = 0, maxValue = 100 } = props; let { progressBarProps } = useProgressBar(props); let center = 16; let strokeWidth = 4; let r = 16 - strokeWidth; let c = 2 * r * Math.PI; let percentage = isIndeterminate ? 0.25 : (value - minValue) / (maxValue - minValue); let offset = c - percentage * c; return ( <svg {...progressBarProps} width={32} height={32} viewBox="0 0 32 32" fill="none" strokeWidth={strokeWidth} > <circle role="presentation" cx={center} cy={center} r={r} stroke="gray" /> <circle role="presentation" cx={center} cy={center} r={r} stroke="orange" strokeDasharray={`${c} ${c}`} strokeDashoffset={offset} transform="rotate(-90 16 16)" > {props.isIndeterminate && ( <animateTransform attributeName="transform" type="rotate" begin="0s" dur="1s" from="0 16 16" to="360 16 16" repeatCount="indefinite" /> )} </circle> </svg> ); } <ProgressCircle aria-label="Loading…" value={60} /> function ProgressCircle( props ) { let { isIndeterminate, value, minValue = 0, maxValue = 100 } = props; let { progressBarProps } = useProgressBar( props ); let center = 16; let strokeWidth = 4; let r = 16 - strokeWidth; let c = 2 * r * Math.PI; let percentage = isIndeterminate ? 0.25 : (value - minValue) / (maxValue - minValue); let offset = c - percentage * c; return ( <svg {...progressBarProps} width={32} height={32} viewBox="0 0 32 32" fill="none" strokeWidth={strokeWidth} > <circle role="presentation" cx={center} cy={center} r={r} stroke="gray" /> <circle role="presentation" cx={center} cy={center} r={r} stroke="orange" strokeDasharray={`${c} ${c}`} strokeDashoffset={offset} transform="rotate(-90 16 16)" > {props .isIndeterminate && ( <animateTransform attributeName="transform" type="rotate" begin="0s" dur="1s" from="0 16 16" to="360 16 16" repeatCount="indefinite" /> )} </circle> </svg> ); } <ProgressCircle aria-label="Loading…" value={60} /> ## Usage# * * * The following examples show how to use the `ProgressBar` and `ProgressCircle` components created in the above example. ### Custom value scale# By default, the `value` prop represents the current percentage of progress, as the minimum and maximum values default to 0 and 100, respectively. Alternatively, a different scale can be used by setting the `minValue` and `maxValue` props. <ProgressBar label="Loading…" minValue={50} maxValue={150} value={100} /> <ProgressBar label="Loading…" minValue={50} maxValue={150} value={100} /> <ProgressBar label="Loading…" minValue={50} maxValue={150} value={100} /> Loading…50% ### Value formatting# Values are formatted as a percentage by default, but this can be modified by using the `formatOptions` prop to specify a different format. `formatOptions` is compatible with the option parameter of Intl.NumberFormat and is applied based on the current locale. <ProgressBar label="Sending…" formatOptions={{style: 'currency', currency: 'JPY'}} value={60} /> <ProgressBar label="Sending…" formatOptions={{style: 'currency', currency: 'JPY'}} value={60} /> <ProgressBar label="Sending…" formatOptions={{ style: 'currency', currency: 'JPY' }} value={60} /> Sending…¥60 ### Custom value label# The `valueLabel` prop allows the formatted value to be replaced with a custom string. <ProgressBar label="Feeding…" valueLabel="30 of 100 dogs" value={30} /> <ProgressBar label="Feeding…" valueLabel="30 of 100 dogs" value={30} /> <ProgressBar label="Feeding…" valueLabel="30 of 100 dogs" value={30} /> Feeding…30 of 100 dogs ### Indeterminate# The `isIndeterminate` prop can be used to represent an indeterminate operation. The `ProgressCircle` component created above implements an animation to indicate this visually. <ProgressCircle aria-label="Loading…" isIndeterminate /> <ProgressCircle aria-label="Loading…" isIndeterminate /> <ProgressCircle aria-label="Loading…" isIndeterminate /> ## Internationalization# * * * ### Value formatting# `useProgressBar` will handle localized formatting of the value label for accessibility automatically. This is returned in the `aria-valuetext` prop in `progressBarProps`. You can use this to create a visible label if needed and ensure that it is formatted correctly. The number formatting can also be controlled using the `formatOptions` prop. ### RTL# In right-to-left languages, the progress bar should be mirrored. The label is right-aligned, the value is left-aligned, and the fill progresses from right to left. Ensure that your CSS accounts for this. --- ## Page: https://react-spectrum.adobe.com/react-aria/useToast.html # useToast Provides the behavior and accessibility implementation for a toast component. Toasts display brief, temporary notifications of actions, errors, or other events in an application. <table><tbody><tr><th>install</th><td><code>yarn add react-aria</code></td></tr><tr><th>version</th><td>3.41.1</td></tr><tr><th>usage</th><td><code><span>import</span> {useToastRegion, useToast} <span>from</span> <span>'react-aria'</span></code></td></tr></tbody></table> View ARIA pattern W3C View repository GitHub View package NPM ## API# * * * `useToastRegion<T>( props: AriaToastRegionProps , state: ToastState <T>, ref: RefObject <HTMLElement | | null> ): ToastRegionAria ``useToast<T>( props: AriaToastProps <T>, state: ToastState <T>, ref: RefObject <FocusableElement | | null> ): ToastAria ` ## Features# * * * There is no built in way to display toast notifications in HTML. `useToastRegion` and` useToast `help achieve accessible toasts that can be styled as needed. * **Accessible** – Toasts follow the ARIA alertdialog pattern. They are rendered in a landmark region, which keyboard and screen reader users can easily jump to when an alert is announced. * **Focus management** – When a toast unmounts, focus is moved to the next toast if any. Otherwise, focus is restored to where it was before navigating to the toast region. Tabbing through the Toast region will move from newest to oldest. ## Anatomy# * * * A toast region is an ARIA landmark region labeled "Notifications" by default. A toast region contains one or more visible toasts, in chronological order. When the limit is reached, additional toasts are queued until the user dismisses one. Each toast is a non-modal ARIA alertdialog, containing the content of the notification and a close button. Landmark regions including the toast container can be navigated using the keyboard by pressing the F6 key to move forward, and the Shift + F6 key to move backward. This provides an easy way for keyboard users to jump to the toasts from anywhere in the app. When the last toast is closed, keyboard focus is restored. `useToastRegion` returns props that you should spread onto the toast container element: | Name | Type | Description | | --- | --- | --- | | `regionProps` | `DOMAttributes` | Props for the landmark region element. | `useToast` returns props that you should spread onto an individual toast and its child elements: | Name | Type | Description | | --- | --- | --- | | `toastProps` | `DOMAttributes` | Props for the toast container, non-modal dialog element. | | `contentProps` | `DOMAttributes` | Props for the toast content alert message. | | `titleProps` | `DOMAttributes` | Props for the toast title element. | | `descriptionProps` | `DOMAttributes` | Props for the toast description element, if any. | | `closeButtonProps` | ` AriaButtonProps ` | Props for the toast close button. | ## Example# * * * Toasts consist of three components. The first is a `ToastProvider` component which will manage the state for the toast queue with the `useToastState` hook. Alternatively, you could use a global toast queue (see below). import {useToastState} from 'react-stately'; function ToastProvider({ children, ...props }) { let state = useToastState({ maxVisibleToasts: 5 }); return ( <> {children(state)} {state.visibleToasts.length > 0 && ( <ToastRegion {...props} state={state} /> )} </> ); } import {useToastState} from 'react-stately'; function ToastProvider({ children, ...props }) { let state = useToastState({ maxVisibleToasts: 5 }); return ( <> {children(state)} {state.visibleToasts.length > 0 && ( <ToastRegion {...props} state={state} /> )} </> ); } import {useToastState} from 'react-stately'; function ToastProvider( { children, ...props } ) { let state = useToastState({ maxVisibleToasts: 5 }); return ( <> {children(state)} {state .visibleToasts .length > 0 && ( <ToastRegion {...props} state={state} /> )} </> ); } The `ToastRegion` component will be rendered when there are toasts to display. It uses the `useToastRegion` hook to create a landmark region, allowing keyboard and screen reader users to easily navigate to it. import type {ToastState} from 'react-stately'; import type {AriaToastRegionProps} from 'react-aria'; import {useToastRegion} from 'react-aria'; interface ToastRegionProps<T> extends AriaToastRegionProps { state: ToastState<T>; } function ToastRegion<T extends React.ReactNode>( { state, ...props }: ToastRegionProps<T> ) { let ref = React.useRef(null); let { regionProps } = useToastRegion(props, state, ref); return ( <div {...regionProps} ref={ref} className="toast-region"> {state.visibleToasts.map((toast) => ( <Toast key={toast.key} toast={toast} state={state} /> ))} </div> ); } import type {ToastState} from 'react-stately'; import type {AriaToastRegionProps} from 'react-aria'; import {useToastRegion} from 'react-aria'; interface ToastRegionProps<T> extends AriaToastRegionProps { state: ToastState<T>; } function ToastRegion<T extends React.ReactNode>( { state, ...props }: ToastRegionProps<T> ) { let ref = React.useRef(null); let { regionProps } = useToastRegion(props, state, ref); return ( <div {...regionProps} ref={ref} className="toast-region" > {state.visibleToasts.map((toast) => ( <Toast key={toast.key} toast={toast} state={state} /> ))} </div> ); } import type {ToastState} from 'react-stately'; import type {AriaToastRegionProps} from 'react-aria'; import {useToastRegion} from 'react-aria'; interface ToastRegionProps< T > extends AriaToastRegionProps { state: ToastState<T>; } function ToastRegion< T extends React.ReactNode >( { state, ...props }: ToastRegionProps<T> ) { let ref = React.useRef( null ); let { regionProps } = useToastRegion( props, state, ref ); return ( <div {...regionProps} ref={ref} className="toast-region" > {state .visibleToasts .map((toast) => ( <Toast key={toast .key} toast={toast} state={state} /> ))} </div> ); } Finally, we need the `Toast` component to render an individual toast within a `ToastRegion`, built with `useToast` . import type {AriaToastProps} from 'react-aria'; import {useToast} from 'react-aria'; // Reuse the Button from your component library. See below for details. import {Button} from 'your-component-library'; interface ToastProps<T> extends AriaToastProps<T> { state: ToastState<T>; } function Toast<T extends React.ReactNode>({ state, ...props }: ToastProps<T>) { let ref = React.useRef(null); let { toastProps, contentProps, titleProps, closeButtonProps } = useToast( props, state, ref ); return ( <div {...toastProps} ref={ref} className="toast"> <div {...contentProps}> <div {...titleProps}>{props.toast.content}</div> </div> <Button {...closeButtonProps}>x</Button> </div> ); } import type {AriaToastProps} from 'react-aria'; import {useToast} from 'react-aria'; // Reuse the Button from your component library. See below for details. import {Button} from 'your-component-library'; interface ToastProps<T> extends AriaToastProps<T> { state: ToastState<T>; } function Toast<T extends React.ReactNode>( { state, ...props }: ToastProps<T> ) { let ref = React.useRef(null); let { toastProps, contentProps, titleProps, closeButtonProps } = useToast(props, state, ref); return ( <div {...toastProps} ref={ref} className="toast"> <div {...contentProps}> <div {...titleProps}>{props.toast.content}</div> </div> <Button {...closeButtonProps}>x</Button> </div> ); } import type {AriaToastProps} from 'react-aria'; import {useToast} from 'react-aria'; // Reuse the Button from your component library. See below for details. import {Button} from 'your-component-library'; interface ToastProps<T> extends AriaToastProps<T> { state: ToastState<T>; } function Toast< T extends React.ReactNode >( { state, ...props }: ToastProps<T> ) { let ref = React.useRef( null ); let { toastProps, contentProps, titleProps, closeButtonProps } = useToast( props, state, ref ); return ( <div {...toastProps} ref={ref} className="toast" > <div {...contentProps} > <div {...titleProps} > {props.toast .content} </div> </div> <Button {...closeButtonProps} > x </Button> </div> ); } <ToastProvider> {state => ( <Button onPress={() => state.add('Toast is done!')}>Show toast</Button> )} </ToastProvider> <ToastProvider> {(state) => ( <Button onPress={() => state.add('Toast is done!')}> Show toast </Button> )} </ToastProvider> <ToastProvider> {(state) => ( <Button onPress={() => state.add( 'Toast is done!' )} > Show toast </Button> )} </ToastProvider> Show toast Show CSS .toast-region { position: fixed; bottom: 16px; right: 16px; display: flex; flex-direction: column-reverse; gap: 8px; } .toast { display: flex; align-items: center; gap: 16px; background: slateblue; color: white; padding: 12px 16px; border-radius: 8px; } .toast button { background: none; border: none; appearance: none; border-radius: 50%; height: 32px; width: 32px; font-size: 16px; border: 1px solid white; color: white; padding: 0; } .toast button:focus-visible { outline: none; box-shadow: 0 0 0 2px slateblue, 0 0 0 4px white; } .toast button:active { background: rgba(255, 255, 255, 0.2); } .toast-region { position: fixed; bottom: 16px; right: 16px; display: flex; flex-direction: column-reverse; gap: 8px; } .toast { display: flex; align-items: center; gap: 16px; background: slateblue; color: white; padding: 12px 16px; border-radius: 8px; } .toast button { background: none; border: none; appearance: none; border-radius: 50%; height: 32px; width: 32px; font-size: 16px; border: 1px solid white; color: white; padding: 0; } .toast button:focus-visible { outline: none; box-shadow: 0 0 0 2px slateblue, 0 0 0 4px white; } .toast button:active { background: rgba(255, 255, 255, 0.2); } .toast-region { position: fixed; bottom: 16px; right: 16px; display: flex; flex-direction: column-reverse; gap: 8px; } .toast { display: flex; align-items: center; gap: 16px; background: slateblue; color: white; padding: 12px 16px; border-radius: 8px; } .toast button { background: none; border: none; appearance: none; border-radius: 50%; height: 32px; width: 32px; font-size: 16px; border: 1px solid white; color: white; padding: 0; } .toast button:focus-visible { outline: none; box-shadow: 0 0 0 2px slateblue, 0 0 0 4px white; } .toast button:active { background: rgba(255, 255, 255, 0.2); } ### Button# The `Button` component is used in the above example to close a toast. It is built using the useButton hook, and can be shared with many other components. Show code import {useButton} from 'react-aria'; function Button(props) { let ref = React.useRef(null); let { buttonProps } = useButton(props, ref); return <button {...buttonProps} ref={ref}>{props.children}</button>; } import {useButton} from 'react-aria'; function Button(props) { let ref = React.useRef(null); let { buttonProps } = useButton(props, ref); return ( <button {...buttonProps} ref={ref}> {props.children} </button> ); } import {useButton} from 'react-aria'; function Button(props) { let ref = React.useRef( null ); let { buttonProps } = useButton( props, ref ); return ( <button {...buttonProps} ref={ref} > {props.children} </button> ); } ## Usage# * * * The following examples show how to use the `ToastProvider` component created in the above example. ### Auto-dismiss# Toasts support a `timeout` option to automatically hide them after a certain amount of time. For accessibility, toasts should have a minimum timeout of 5 seconds to give users enough time to read them. If a toast includes action buttons or other interactive elements it should not auto dismiss. In addition, timers will automatically pause when the user focuses or hovers over a toast. Be sure only to automatically dismiss toasts when the information is not important, or may be found elsewhere. Some users may require additional time to read a toast message, and screen zoom users may miss toasts entirely. <ToastProvider> {state => ( <Button onPress={() => state.add('Toast still toasting!', {timeout: 5000})}> Show toast </Button> )} </ToastProvider> <ToastProvider> {(state) => ( <Button onPress={() => state.add('Toast still toasting!', { timeout: 5000 })} > Show toast </Button> )} </ToastProvider> <ToastProvider> {(state) => ( <Button onPress={() => state.add( 'Toast still toasting!', { timeout: 5000 } )} > Show toast </Button> )} </ToastProvider> Show toast ### Programmatic dismissal# Toasts may be programmatically dismissed if they become irrelevant before the user manually closes them. `state.add` returns a key for the toast which may be passed to `state.close` to dismiss the toast. function Example() { let [toastKey, setToastKey] = React.useState(null); return ( <ToastProvider> {(state) => ( <Button onPress={() => { if (!toastKey) { setToastKey( state.add('Unable to save', { onClose: () => setToastKey(null) }) ); } else { state.close(toastKey); } }} > {toastKey ? 'Hide' : 'Show'} Toast </Button> )} </ToastProvider> ); } function Example() { let [toastKey, setToastKey] = React.useState(null); return ( <ToastProvider> {(state) => ( <Button onPress={() => { if (!toastKey) { setToastKey( state.add('Unable to save', { onClose: () => setToastKey(null) }) ); } else { state.close(toastKey); } }} > {toastKey ? 'Hide' : 'Show'} Toast </Button> )} </ToastProvider> ); } function Example() { let [ toastKey, setToastKey ] = React.useState( null ); return ( <ToastProvider> {(state) => ( <Button onPress={() => { if ( !toastKey ) { setToastKey( state .add( 'Unable to save', { onClose: () => setToastKey( null ) } ) ); } else { state .close( toastKey ); } }} > {toastKey ? 'Hide' : 'Show'} {' '} Toast </Button> )} </ToastProvider> ); } Show Toast ## Advanced topics# * * * ### Global toast queue# In the above examples, each `ToastProvider` has a separate queue. This setup is simple, and fine for most cases where you can wrap the entire app in a single `ToastProvider`. However, in more complex situations, you may want to keep the toast queue outside the React tree so that toasts can be queued from anywhere. This can be done by creating your own `ToastQueue` and subscribing to it using the` useToastQueue `hook rather than `useToastState`. import {ToastQueue, useToastQueue} from 'react-stately'; import {createPortal} from 'react-dom'; // Create a global toast queue. const toastQueue = new ToastQueue({ maxVisibleToasts: 5 }); function GlobalToastRegion(props) { // Subscribe to it. let state = useToastQueue(toastQueue); // Render toast region. return state.visibleToasts.length > 0 ? createPortal(<ToastRegion {...props} state={state} />, document.body) : null; } // Render it somewhere in your app. <GlobalToastRegion /> import {ToastQueue, useToastQueue} from 'react-stately'; import {createPortal} from 'react-dom'; // Create a global toast queue. const toastQueue = new ToastQueue({ maxVisibleToasts: 5 }); function GlobalToastRegion(props) { // Subscribe to it. let state = useToastQueue(toastQueue); // Render toast region. return state.visibleToasts.length > 0 ? createPortal( <ToastRegion {...props} state={state} />, document.body ) : null; } // Render it somewhere in your app. <GlobalToastRegion /> import { ToastQueue, useToastQueue } from 'react-stately'; import {createPortal} from 'react-dom'; // Create a global toast queue. const toastQueue = new ToastQueue({ maxVisibleToasts: 5 }); function GlobalToastRegion( props ) { // Subscribe to it. let state = useToastQueue( toastQueue ); // Render toast region. return state .visibleToasts .length > 0 ? createPortal( <ToastRegion {...props} state={state} />, document.body ) : null; } // Render it somewhere in your app. <GlobalToastRegion /> Now you can queue a toast from anywhere: <Button onPress={() => toastQueue.add('Toast is done!')}>Show toast</Button> <Button onPress={() => toastQueue.add('Toast is done!')}> Show toast </Button> <Button onPress={() => toastQueue.add( 'Toast is done!' )} > Show toast </Button> Show toast ### TypeScript# A `ToastQueue` and `useToastState` use a generic type to represent toast content. The examples so far have used strings, but you can type this however you want to enable passing custom objects or options. This example uses a custom object to support toasts with both a title and description. import type {QueuedToast} from 'react-stately'; interface MyToast { title: string; description: string; } function ToastProvider() { let state = useToastState<MyToast>(); // ... } interface ToastProps { toast: QueuedToast<MyToast>;} function Toast(props: ToastProps) { // ... let { toastProps, titleProps, descriptionProps, closeButtonProps } = useToast( props, state, ref ); return ( <div {...toastProps} ref={ref} className="toast"> <div> <div {...titleProps}>{props.toast.content.title}</div> <div {...descriptionProps}>{props.toast.content.description}</div> </div> <Button {...closeButtonProps}>x</Button> </div> ); } // Queuing a toast state.add({ title: 'Success!', description: 'Toast is done.' }); import type {QueuedToast} from 'react-stately'; interface MyToast { title: string; description: string; } function ToastProvider() { let state = useToastState<MyToast>(); // ... } interface ToastProps { toast: QueuedToast<MyToast>;} function Toast(props: ToastProps) { // ... let { toastProps, titleProps, descriptionProps, closeButtonProps } = useToast(props, state, ref); return ( <div {...toastProps} ref={ref} className="toast"> <div> <div {...titleProps}> {props.toast.content.title} </div> <div {...descriptionProps}> {props.toast.content.description} </div> </div> <Button {...closeButtonProps}>x</Button> </div> ); } // Queuing a toast state.add({ title: 'Success!', description: 'Toast is done.' }); import type {QueuedToast} from 'react-stately'; interface MyToast { title: string; description: string; } function ToastProvider() { let state = useToastState< MyToast >(); // ... } interface ToastProps { toast: QueuedToast< MyToast >;} function Toast( props: ToastProps ) { // ... let { toastProps, titleProps, descriptionProps, closeButtonProps } = useToast( props, state, ref ); return ( <div {...toastProps} ref={ref} className="toast" > <div> <div {...titleProps} > {props.toast .content .title} </div> <div {...descriptionProps} > {props.toast .content .description} </div> </div> <Button {...closeButtonProps} > x </Button> </div> ); } // Queuing a toast state.add({ title: 'Success!', description: 'Toast is done.' }); --- ## Page: https://react-spectrum.adobe.com/react-aria/useSeparator.html # useSeparator Provides the accessibility implementation for a separator. A separator is a visual divider between two groups of content, e.g. groups of menu items or sections of a page. <table><tbody><tr><th>install</th><td><code>yarn add react-aria</code></td></tr><tr><th>version</th><td>3.41.1</td></tr><tr><th>usage</th><td><code><span>import</span> {useSeparator} <span>from</span> <span>'react-aria'</span></code></td></tr></tbody></table> View ARIA pattern W3C View repository GitHub View package NPM ## API# * * * `useSeparator( (props: SeparatorProps )): SeparatorAria ` ## Features# * * * Horizontal separators can be built with the HTML <hr> element. However, there is no HTML element for a vertical separator. `useSeparator` helps achieve accessible separators that can be styled as needed. * Support for horizontal and vertical orientation * Support for HTML `<hr>` element or a custom element type ## Anatomy# * * * A separator consists of a single element that represents the divider. `useSeparator` returns props to be spread onto the element: | Name | Type | Description | | --- | --- | --- | | `separatorProps` | `DOMAttributes` | Props for the separator element. | ## Example# * * * This example shows how create both a horizontal and a vertical oriented separator. import {useSeparator} from 'react-aria'; function Separator(props) { let { separatorProps } = useSeparator(props); return ( <div {...separatorProps} style={{ background: 'gray', width: props.orientation === 'vertical' ? '1px' : '100%', height: props.orientation === 'vertical' ? '100%' : '1px', margin: props.orientation === 'vertical' ? '0 5px' : '5px 0' }} /> ); } <> <div style={{ display: 'flex', flexDirection: 'column' }}> Content above <Separator /> Content below </div> <div style={{ display: 'flex', flexDirection: 'row', marginTop: 20, height: 40, alignItems: 'center' }} > Content left <Separator orientation="vertical" /> Content right </div> </> import {useSeparator} from 'react-aria'; function Separator(props) { let { separatorProps } = useSeparator(props); return ( <div {...separatorProps} style={{ background: 'gray', width: props.orientation === 'vertical' ? '1px' : '100%', height: props.orientation === 'vertical' ? '100%' : '1px', margin: props.orientation === 'vertical' ? '0 5px' : '5px 0' }} /> ); } <> <div style={{ display: 'flex', flexDirection: 'column' }} > Content above <Separator /> Content below </div> <div style={{ display: 'flex', flexDirection: 'row', marginTop: 20, height: 40, alignItems: 'center' }} > Content left <Separator orientation="vertical" /> Content right </div> </> import {useSeparator} from 'react-aria'; function Separator( props ) { let { separatorProps } = useSeparator( props ); return ( <div {...separatorProps} style={{ background: 'gray', width: props .orientation === 'vertical' ? '1px' : '100%', height: props .orientation === 'vertical' ? '100%' : '1px', margin: props .orientation === 'vertical' ? '0 5px' : '5px 0' }} /> ); } <> <div style={{ display: 'flex', flexDirection: 'column' }} > Content above <Separator /> Content below </div> <div style={{ display: 'flex', flexDirection: 'row', marginTop: 20, height: 40, alignItems: 'center' }} > Content left <Separator orientation="vertical" /> Content right </div> </> Content above Content below Content left Content right --- ## Page: https://react-spectrum.adobe.com/react-aria/useToolbar.html # useToolbar Provides the behavior and accessibility implementation for a toolbar. A toolbar is a container for a set of interactive controls with arrow key navigation. <table><tbody><tr><th>install</th><td><code>yarn add @react-aria/toolbar</code></td></tr><tr><th>version</th><td>3.0.0-beta.18</td></tr><tr><th>usage</th><td><code><span>import</span> {useToolbar} <span>from</span> <span>'@react-aria/toolbar'</span></code></td></tr></tbody></table> View ARIA pattern W3C View repository GitHub View package NPM ## API# * * * `useToolbar( (props: AriaToolbarProps , , ref: RefObject <HTMLElement | | null> )): ToolbarAria ` ## Features# * * * There is no native element to implement a toolbar in HTML. `useToolbar` helps achieve accessible toolbar components that can be styled as needed. * Exposed to assistive technology as a `toolbar` element via ARIA * Support for arrow key navigation * Support for both horizontal and vertical orientations * Support for interactive children including button, toggle button, menu, checkbox, and link * Automatic scrolling support during keyboard navigation ## Anatomy# * * * A toolbar consists of a container element for a set of interactive controls. `useToolbar` handles exposing this to assistive technology using ARIA, along with handling arrow key navigation between children. `useToolbar` returns props that you should spread onto the appropriate element: | Name | Type | Description | | --- | --- | --- | | `toolbarProps` | `HTMLAttributes<HTMLElement>` | Props for the toolbar container. | An `aria-label` or `aria-labelledby` prop must be provided to identify the toolbar to assistive technology. ## Example# * * * ### Toolbar# This example uses the `useToolbar` hook, spread on a container to handle navigation of components inside it. import {useToolbar} from 'react-aria'; import {useRef} from 'react'; // Reuse the Button from your component library. See below for details. import {Button} from 'your-component-library'; function Toolbar(props) { let ref = useRef<HTMLDivElement | null>(null); // Get props for the toolbar element let { toolbarProps } = useToolbar(props, ref); return ( <div {...toolbarProps} ref={ref}> {props.children} </div> ); } <Toolbar aria-label="Actions"> <Button>Copy</Button> <Button>Cut</Button> <Button>Paste</Button> </Toolbar> import {useToolbar} from 'react-aria'; import {useRef} from 'react'; // Reuse the Button from your component library. See below for details. import {Button} from 'your-component-library'; function Toolbar(props) { let ref = useRef<HTMLDivElement | null>(null); // Get props for the toolbar element let { toolbarProps } = useToolbar(props, ref); return ( <div {...toolbarProps} ref={ref}> {props.children} </div> ); } <Toolbar aria-label="Actions"> <Button>Copy</Button> <Button>Cut</Button> <Button>Paste</Button> </Toolbar> import {useToolbar} from 'react-aria'; import {useRef} from 'react'; // Reuse the Button from your component library. See below for details. import {Button} from 'your-component-library'; function Toolbar(props) { let ref = useRef< HTMLDivElement | null >(null); // Get props for the toolbar element let { toolbarProps } = useToolbar( props, ref ); return ( <div {...toolbarProps} ref={ref} > {props.children} </div> ); } <Toolbar aria-label="Actions"> <Button>Copy</Button> <Button>Cut</Button> <Button> Paste </Button> </Toolbar> CopyCutPaste Show CSS [role=toolbar] { --separator-color: var(--spectrum-global-color-gray-500); display: flex; flex-wrap: wrap; gap: 5px; } [role=toolbar] { --separator-color: var(--spectrum-global-color-gray-500); display: flex; flex-wrap: wrap; gap: 5px; } [role=toolbar] { --separator-color: var(--spectrum-global-color-gray-500); display: flex; flex-wrap: wrap; gap: 5px; } ### Button# The `Button` component is used in the above example as an interactive child. It is built using the useButton hook, and can be shared with many other components. Show code import {useButton} from 'react-aria'; function Button(props) { let { children } = props; let ref = useRef<HTMLButtonElement | null>(null); let { buttonProps, isPressed } = useButton({ ...props, elementType: 'span' }, ref); return ( <span {...buttonProps} style={{ background: isPressed ? '#bbb' : '#aaa', color: 'black', cursor: 'default', padding: '5px 10px' }} ref={ref} > {children} </span> ); } import {useButton} from 'react-aria'; function Button(props) { let { children } = props; let ref = useRef<HTMLButtonElement | null>(null); let { buttonProps, isPressed } = useButton({ ...props, elementType: 'span' }, ref); return ( <span {...buttonProps} style={{ background: isPressed ? '#bbb' : '#aaa', color: 'black', cursor: 'default', padding: '5px 10px' }} ref={ref} > {children} </span> ); } import {useButton} from 'react-aria'; function Button(props) { let { children } = props; let ref = useRef< | HTMLButtonElement | null >(null); let { buttonProps, isPressed } = useButton({ ...props, elementType: 'span' }, ref); return ( <span {...buttonProps} style={{ background: isPressed ? '#bbb' : '#aaa', color: 'black', cursor: 'default', padding: '5px 10px' }} ref={ref} > {children} </span> ); } ## Internationalization# * * * You are responsible for localizing all labels, both for the toolbar itself as well as all the content that is passed into the toolbar. ### RTL# In right-to-left languages, the toolbar should be mirrored both at the toolbar level as well as inside groups as appropriate. Ensure that your CSS accounts for this. --- ## Page: https://react-spectrum.adobe.com/react-aria/useFocus.html # useFocus Handles focus events for the immediate target. Focus events on child elements will be ignored. <table><tbody><tr><th>install</th><td><code>yarn add react-aria</code></td></tr><tr><th>version</th><td>3.41.1</td></tr><tr><th>usage</th><td><code><span>import</span> {useFocus} <span>from</span> <span>'react-aria'</span></code></td></tr></tbody></table> View repository GitHub View package NPM ## API# * * * `useFocus<Target extends FocusableElement = FocusableElement>( (props: FocusProps <Target> )): FocusResult <Target>` ## Features# * * * `useFocus` handles focus interactions for an element. Unlike React's built-in focus events, `useFocus` does not fire focus events for child elements of the target. This matches DOM behavior where focus events do not bubble. This is similar to the :focus pseudo class in CSS. To handle focus events on descendants of an element, see useFocusWithin. ## Usage# * * * `useFocus` returns props that you should spread onto the target element: | Name | Type | Description | | --- | --- | --- | | `focusProps` | `DOMAttributes<Target>` | Props to spread onto the target element. | `useFocus` supports the following event handlers: | Name | Type | Description | | --- | --- | --- | | `onFocus` | `( (e: FocusEvent<Target> )) => void` | Handler that is called when the element receives focus. | | `onBlur` | `( (e: FocusEvent<Target> )) => void` | Handler that is called when the element loses focus. | | `onFocusChange` | `( (isFocused: boolean )) => void` | Handler that is called when the element's focus status changes. | ## Example# * * * This example shows a simple input element that handles focus events with `useFocus` and logs them to a list below. **NOTE: for more advanced text field functionality, see useTextField.** import {useFocus} from 'react-aria'; function Example() { let [events, setEvents] = React.useState([]); let { focusProps } = useFocus({ onFocus: (e) => setEvents( (events) => [...events, 'focus'] ), onBlur: (e) => setEvents( (events) => [...events, 'blur'] ), onFocusChange: (isFocused) => setEvents( (events) => [...events, `focus change: ${isFocused}`] ) }); return ( <> <label htmlFor="example">Example</label> <input {...focusProps} id="example" /> <ul style={{ maxHeight: '200px', overflow: 'auto' }} > {events.map((e, i) => <li key={i}>{e}</li>)} </ul> </> ); } import {useFocus} from 'react-aria'; function Example() { let [events, setEvents] = React.useState([]); let { focusProps } = useFocus({ onFocus: (e) => setEvents( (events) => [...events, 'focus'] ), onBlur: (e) => setEvents( (events) => [...events, 'blur'] ), onFocusChange: (isFocused) => setEvents( (events) => [ ...events, `focus change: ${isFocused}` ] ) }); return ( <> <label htmlFor="example">Example</label> <input {...focusProps} id="example" /> <ul style={{ maxHeight: '200px', overflow: 'auto' }} > {events.map((e, i) => <li key={i}>{e}</li>)} </ul> </> ); } import {useFocus} from 'react-aria'; function Example() { let [ events, setEvents ] = React.useState([]); let { focusProps } = useFocus({ onFocus: (e) => setEvents( (events) => [ ...events, 'focus' ] ), onBlur: (e) => setEvents( (events) => [ ...events, 'blur' ] ), onFocusChange: (isFocused) => setEvents( (events) => [ ...events, `focus change: ${isFocused}` ] ) }); return ( <> <label htmlFor="example"> Example </label> <input {...focusProps} id="example" /> <ul style={{ maxHeight: '200px', overflow: 'auto' }} > {events.map(( e, i ) => ( <li key={i}> {e} </li> ))} </ul> </> ); } Example --- ## Page: https://react-spectrum.adobe.com/react-aria/useFocusVisible.html # useFocusVisible Manages focus visible state for the page, and subscribes individual components for updates. <table><tbody><tr><th>install</th><td><code>yarn add react-aria</code></td></tr><tr><th>version</th><td>3.41.1</td></tr><tr><th>usage</th><td><code><span>import</span> {useFocusVisible} <span>from</span> <span>'react-aria'</span></code></td></tr></tbody></table> View repository GitHub View package NPM ## API# * * * `useFocusVisible( (props: FocusVisibleProps )): FocusVisibleResult ` ## Features# * * * `useFocusVisible` handles focus interactions for the page and determines whether keyboard focus should be visible (e.g. with a focus ring). Focus visibility is computed based on the current interaction mode of the user. When the user interacts via a mouse or touch, then focus is not visible. When the user interacts via a keyboard or screen reader, then focus is visible. This is similar to the :focus-visible pseudo class in CSS. To determine whether a focus ring should be visible for an individual component rather than globally, see useFocusRing. ## Example# * * * This example shows focus visible state and updates as you interact with the page. By default, when the page loads, it is true. If you press anywhere on the page with a mouse or touch, then focus visible state is set to false. If you keyboard navigate around the page then it is set to true again. Note that this example uses the `isTextInput` option so that only certain navigation keys cause focus visible state to appear. This prevents focus visible state from appearing when typing text in a text field. import {useFocusVisible} from 'react-aria'; function Example() { let { isFocusVisible } = useFocusVisible({ isTextInput: true }); return ( <> <div>Focus visible: {String(isFocusVisible)}</div> <label style={{ display: 'block' }}> First Name: <input /> </label> <label style={{ display: 'block' }}> Last Name: <input /> </label> </> ); } import {useFocusVisible} from 'react-aria'; function Example() { let { isFocusVisible } = useFocusVisible({ isTextInput: true }); return ( <> <div>Focus visible: {String(isFocusVisible)}</div> <label style={{ display: 'block' }}> First Name: <input /> </label> <label style={{ display: 'block' }}> Last Name: <input /> </label> </> ); } import {useFocusVisible} from 'react-aria'; function Example() { let { isFocusVisible } = useFocusVisible({ isTextInput: true }); return ( <> <div> Focus visible: {' '} {String( isFocusVisible )} </div> <label style={{ display: 'block' }} > First Name:{' '} <input /> </label> <label style={{ display: 'block' }} > Last Name:{' '} <input /> </label> </> ); } Focus visible: true First Name: Last Name: --- ## Page: https://react-spectrum.adobe.com/react-aria/useFocusWithin.html # useFocusWithin Handles focus events for the target and its descendants. <table><tbody><tr><th>install</th><td><code>yarn add react-aria</code></td></tr><tr><th>version</th><td>3.41.1</td></tr><tr><th>usage</th><td><code><span>import</span> {useFocusWithin} <span>from</span> <span>'react-aria'</span></code></td></tr></tbody></table> View repository GitHub View package NPM ## API# * * * `useFocusWithin( (props: FocusWithinProps )): FocusWithinResult ` ## Features# * * * `useFocusWithin` handles focus interactions for an element and its descendants. Focus is "within" an element when either the element itself or a descendant element has focus. This is similar to the :focus-within pseudo class in CSS. To handle focus events on only the target element, and not descendants, see useFocus. ## Usage# * * * `useFocusWithin` returns props that you should spread onto the target element: | Name | Type | Description | | --- | --- | --- | | `focusWithinProps` | `DOMAttributes` | Props to spread onto the target element. | `useFocusWithin` supports the following event handlers: | Name | Type | Description | | --- | --- | --- | | `onFocusWithin` | `( (e: FocusEvent )) => void` | Handler that is called when the target element or a descendant receives focus. | | `onBlurWithin` | `( (e: FocusEvent )) => void` | Handler that is called when the target element and all descendants lose focus. | | `onFocusWithinChange` | `( (isFocusWithin: boolean )) => void` | Handler that is called when the the focus within state changes. | ## Example# * * * This example shows two text fields inside a div, which handles focus within events. It stores focus within state in local component state, which is updated by an `onFocusWithinChange` handler. This is used to update the background color and text color of the group while one of the text fields has focus. Focus within and blur within events are also logged to the list below. Notice that the events are only fired when the wrapper gains and loses focus, not when focus moves within the group. **NOTE: for more advanced text field functionality, see useTextField.** import {useFocusWithin} from 'react-aria'; function Example() { let [events, setEvents] = React.useState([]); let [isFocusWithin, setFocusWithin] = React.useState(false); let { focusWithinProps } = useFocusWithin({ onFocusWithin: (e) => setEvents( (events) => [...events, 'focus within'] ), onBlurWithin: (e) => setEvents( (events) => [...events, 'blur within'] ), onFocusWithinChange: (isFocusWithin) => setFocusWithin(isFocusWithin) }); return ( <> <div {...focusWithinProps} style={{ display: 'inline-block', border: '1px solid gray', padding: 10, background: isFocusWithin ? 'goldenrod' : '', color: isFocusWithin ? 'black' : '' }} > <label style={{ display: 'block' }}> First Name: <input /> </label> <label style={{ display: 'block' }}> Last Name: <input /> </label> </div> <ul style={{ maxHeight: '200px', overflow: 'auto' }} > {events.map((e, i) => <li key={i}>{e}</li>)} </ul> </> ); } import {useFocusWithin} from 'react-aria'; function Example() { let [events, setEvents] = React.useState([]); let [isFocusWithin, setFocusWithin] = React.useState( false ); let { focusWithinProps } = useFocusWithin({ onFocusWithin: (e) => setEvents( (events) => [...events, 'focus within'] ), onBlurWithin: (e) => setEvents( (events) => [...events, 'blur within'] ), onFocusWithinChange: (isFocusWithin) => setFocusWithin(isFocusWithin) }); return ( <> <div {...focusWithinProps} style={{ display: 'inline-block', border: '1px solid gray', padding: 10, background: isFocusWithin ? 'goldenrod' : '', color: isFocusWithin ? 'black' : '' }} > <label style={{ display: 'block' }}> First Name: <input /> </label> <label style={{ display: 'block' }}> Last Name: <input /> </label> </div> <ul style={{ maxHeight: '200px', overflow: 'auto' }} > {events.map((e, i) => <li key={i}>{e}</li>)} </ul> </> ); } import {useFocusWithin} from 'react-aria'; function Example() { let [ events, setEvents ] = React.useState([]); let [ isFocusWithin, setFocusWithin ] = React.useState( false ); let { focusWithinProps } = useFocusWithin({ onFocusWithin: (e) => setEvents( (events) => [ ...events, 'focus within' ] ), onBlurWithin: (e) => setEvents( (events) => [ ...events, 'blur within' ] ), onFocusWithinChange: (isFocusWithin) => setFocusWithin( isFocusWithin ) }); return ( <> <div {...focusWithinProps} style={{ display: 'inline-block', border: '1px solid gray', padding: 10, background: isFocusWithin ? 'goldenrod' : '', color: isFocusWithin ? 'black' : '' }} > <label style={{ display: 'block' }} > First Name: {' '} <input /> </label> <label style={{ display: 'block' }} > Last Name:{' '} <input /> </label> </div> <ul style={{ maxHeight: '200px', overflow: 'auto' }} > {events.map(( e, i ) => ( <li key={i}> {e} </li> ))} </ul> </> ); } First Name: Last Name: --- ## Page: https://react-spectrum.adobe.com/react-aria/useHover.html # useHover Handles pointer hover interactions for an element. Normalizes behavior across browsers and platforms, and ignores emulated mouse events on touch devices. <table><tbody><tr><th>install</th><td><code>yarn add react-aria</code></td></tr><tr><th>version</th><td>3.41.1</td></tr><tr><th>usage</th><td><code><span>import</span> {useHover} <span>from</span> <span>'react-aria'</span></code></td></tr></tbody></table> View repository GitHub View package NPM ## API# * * * `useHover( (props: HoverProps )): HoverResult ` ## Features# * * * `useHover` handles hover interactions for an element. A hover interaction begins when a user moves their pointer over an element, and ends when they move their pointer off of the element. * Uses pointer events where available, with fallbacks to mouse and touch events * Ignores emulated mouse events in mobile browsers `useHover` is similar to the :hover pseudo class in CSS, but `:hover` is problematic on touch devices due to mouse emulation in mobile browsers. Depending on the browser and device, `:hover` may never apply, or may apply continuously until the user touches another element. `useHover` only applies when the pointer is truly capable of hovering, and emulated mouse events are ignored. Read our blog post about the complexities of hover event handling to learn more. ## Usage# * * * `useHover` returns props that you should spread onto the target element: | Name | Type | Description | | --- | --- | --- | | `hoverProps` | `DOMAttributes` | Props to spread on the target element. | | `isHovered` | `boolean` | | `useHover` supports the following event handlers: | Name | Type | Description | | --- | --- | --- | | `onHoverStart` | `( (e: HoverEvent )) => void` | Handler that is called when a hover interaction starts. | | `onHoverEnd` | `( (e: HoverEvent )) => void` | Handler that is called when a hover interaction ends. | | `onHoverChange` | `( (isHovering: boolean )) => void` | Handler that is called when the hover state changes. | Each of these handlers is fired with a `HoverEvent`, which exposes information about the target and the type of event that triggered the interaction. | Name | Type | Description | | --- | --- | --- | | `type` | `'hoverstart' | 'hoverend'` | The type of hover event being fired. | | `pointerType` | `'mouse' | 'pen'` | The pointer type that triggered the hover event. | | `target` | `HTMLElement` | The target element of the hover event. | ## Accessibility# * * * Hover interactions should never be the only way to interact with an element because they are not supported across all devices. Alternative interactions should be provided on touch devices, for example a long press or an explicit button to tap. In addition, even on devices with hover support, users may be using a keyboard or screen reader to navigate your app, which also do not trigger hover events. Hover interactions should be paired with focus events in order to expose the content to keyboard users. ## Example# * * * This example shows a simple target that handles hover events with `useHover` and logs them to a list below. It also uses the `isHovered` state to adjust the background color when the target is hovered. import {useHover} from 'react-aria'; function Example() { let [events, setEvents] = React.useState([]); let { hoverProps, isHovered } = useHover({ onHoverStart: (e) => setEvents( (events) => [...events, `hover start with ${e.pointerType}`] ), onHoverEnd: (e) => setEvents( (events) => [...events, `hover end with ${e.pointerType}`] ) }); return ( <> <div {...hoverProps} style={{ background: isHovered ? 'darkgreen' : 'green', color: 'white', display: 'inline-block', padding: 4, cursor: 'pointer' }} role="button" tabIndex={0} > Hover me! </div> <ul style={{ maxHeight: '200px', overflow: 'auto' }} > {events.map((e, i) => <li key={i}>{e}</li>)} </ul> </> ); } import {useHover} from 'react-aria'; function Example() { let [events, setEvents] = React.useState([]); let { hoverProps, isHovered } = useHover({ onHoverStart: (e) => setEvents( (events) => [ ...events, `hover start with ${e.pointerType}` ] ), onHoverEnd: (e) => setEvents( (events) => [ ...events, `hover end with ${e.pointerType}` ] ) }); return ( <> <div {...hoverProps} style={{ background: isHovered ? 'darkgreen' : 'green', color: 'white', display: 'inline-block', padding: 4, cursor: 'pointer' }} role="button" tabIndex={0} > Hover me! </div> <ul style={{ maxHeight: '200px', overflow: 'auto' }} > {events.map((e, i) => <li key={i}>{e}</li>)} </ul> </> ); } import {useHover} from 'react-aria'; function Example() { let [ events, setEvents ] = React.useState([]); let { hoverProps, isHovered } = useHover({ onHoverStart: (e) => setEvents( (events) => [ ...events, `hover start with ${e.pointerType}` ] ), onHoverEnd: (e) => setEvents( (events) => [ ...events, `hover end with ${e.pointerType}` ] ) }); return ( <> <div {...hoverProps} style={{ background: isHovered ? 'darkgreen' : 'green', color: 'white', display: 'inline-block', padding: 4, cursor: 'pointer' }} role="button" tabIndex={0} > Hover me! </div> <ul style={{ maxHeight: '200px', overflow: 'auto' }} > {events.map(( e, i ) => ( <li key={i}> {e} </li> ))} </ul> </> ); } Hover me! --- ## Page: https://react-spectrum.adobe.com/react-aria/useKeyboard.html # useKeyboard Handles keyboard interactions for a focusable element. <table><tbody><tr><th>install</th><td><code>yarn add react-aria</code></td></tr><tr><th>version</th><td>3.41.1</td></tr><tr><th>usage</th><td><code><span>import</span> {useKeyboard} <span>from</span> <span>'react-aria'</span></code></td></tr></tbody></table> View repository GitHub View package NPM ## API# * * * `useKeyboard( (props: KeyboardProps )): KeyboardResult ` ## Features# * * * `useKeyboard` handles keyboard interactions. The only difference from DOM events is that propagation is stopped by default if there is an event handler, unless `event.continuePropagation()` is called. This provides better modularity by default, so that a parent component doesn't respond to an event that a child already handled. If the child doesn't handle the event (e.g. it was for an unknown key), it can call `event.continuePropagation()` to allow parents to handle the event. ## Usage# * * * `useKeyboard` returns props that you should spread onto the target element: | Name | Type | Description | | --- | --- | --- | | `keyboardProps` | `DOMAttributes` | Props to spread onto the target element. | `useKeyboard` supports the following event handlers: | Name | Type | Description | | --- | --- | --- | | `onKeyDown` | `( (e: KeyboardEvent )) => void` | Handler that is called when a key is pressed. | | `onKeyUp` | `( (e: KeyboardEvent )) => void` | Handler that is called when a key is released. | ## Example# * * * This example shows a simple input element that handles keyboard events with `useKeyboard` and logs them to a list below. **NOTE: for more advanced text field functionality, see useTextField.** import {useKeyboard} from 'react-aria'; function Example() { let [events, setEvents] = React.useState([]); let { keyboardProps } = useKeyboard({ onKeyDown: (e) => setEvents( (events) => [`key down: ${e.key}`, ...events] ), onKeyUp: (e) => setEvents( (events) => [`key up: ${e.key}`, ...events] ) }); return ( <> <label htmlFor="example">Example</label> <input {...keyboardProps} id="example" /> <ul style={{ height: 100, overflow: 'auto', border: '1px solid gray', width: 200 }} > {events.map((e, i) => <li key={i}>{e}</li>)} </ul> </> ); } import {useKeyboard} from 'react-aria'; function Example() { let [events, setEvents] = React.useState([]); let { keyboardProps } = useKeyboard({ onKeyDown: (e) => setEvents( (events) => [`key down: ${e.key}`, ...events] ), onKeyUp: (e) => setEvents( (events) => [`key up: ${e.key}`, ...events] ) }); return ( <> <label htmlFor="example">Example</label> <input {...keyboardProps} id="example" /> <ul style={{ height: 100, overflow: 'auto', border: '1px solid gray', width: 200 }} > {events.map((e, i) => <li key={i}>{e}</li>)} </ul> </> ); } import {useKeyboard} from 'react-aria'; function Example() { let [ events, setEvents ] = React.useState([]); let { keyboardProps } = useKeyboard({ onKeyDown: (e) => setEvents( (events) => [ `key down: ${e.key}`, ...events ] ), onKeyUp: (e) => setEvents( (events) => [ `key up: ${e.key}`, ...events ] ) }); return ( <> <label htmlFor="example"> Example </label> <input {...keyboardProps} id="example" /> <ul style={{ height: 100, overflow: 'auto', border: '1px solid gray', width: 200 }} > {events.map(( e, i ) => ( <li key={i}> {e} </li> ))} </ul> </> ); } Example --- ## Page: https://react-spectrum.adobe.com/react-aria/useLandmark.html # useLandmark Provides landmark navigation in an application. Call this with a role and label to register a landmark navigable with F6. <table><tbody><tr><th>install</th><td><code>yarn add react-aria</code></td></tr><tr><th>version</th><td>3.41.1</td></tr><tr><th>usage</th><td><code><span>import</span> {useLandmark} <span>from</span> <span>'react-aria'</span></code></td></tr></tbody></table> View ARIA pattern W3C View repository GitHub View package NPM ## API# * * * `useLandmark( (props: AriaLandmarkProps , , ref: RefObject <FocusableElement | | null> )): LandmarkAria ` ## Features# * * * Landmarks provide a way to designate important subsections of a page. They allow screen reader users to get an overview of the various sections of the page, and jump to a specific section. By default, browsers do not provide a consistent way to navigate between landmarks using the keyboard. The `useLandmark` hook enables keyboard navigation between landmarks, and provides a consistent experience across browsers. * F6 and Shift+F6 key navigation between landmarks * Alt+F6 key navigation to the main landmark * Support for navigating nested landmarks ## Anatomy# * * * Landmark elements can be registered with the `useLandmark` hook. The `role` prop is required. Pressing F6 will move focus to the next landmark on the page, and pressing Shift+F6 will move focus to the previous landmark. If an element within a landmark was previously focused before leaving that landmark, focus will return to that element when navigating back to that landmark. Alt+F6 will always move focus to the main landmark if it has been registered. If multiple landmarks are registered with the same role, they should have unique labels, which can be provided by aria-label or aria-labelledby. For an example of landmarks in use, see the useToastRegion documentation. ## Example# * * * import {useLandmark} from 'react-aria'; import {useRef} from 'react'; function Navigation(props) { let ref = useRef<HTMLElement | null>(null); let { landmarkProps } = useLandmark({ ...props, role: 'navigation' }, ref); return ( <nav ref={ref} {...props} {...landmarkProps}> {props.children} </nav> ); } function Region(props) { let ref = useRef<HTMLElement | null>(null); let { landmarkProps } = useLandmark({ ...props, role: 'region' }, ref); return ( <article ref={ref} {...props} {...landmarkProps}> {props.children} </article> ); } function Search(props) { let ref = useRef<HTMLFormElement | null>(null); let { landmarkProps } = useLandmark({ ...props, role: 'search' }, ref); return ( <form ref={ref} {...props} {...landmarkProps}> <h2 id="search-header">Search</h2> <input aria-labelledby="search-header" type="search" /> </form> ); } <div> <Navigation> <h2>Navigation</h2> <ul> <li> <a href="#">Link 1</a> </li> <li> <a href="#">Link 2</a> </li> </ul> </Navigation> <Search /> <Region aria-label="Example region"> <h2>Region</h2> <p>Example region with no focusable children.</p> </Region> </div> import {useLandmark} from 'react-aria'; import {useRef} from 'react'; function Navigation(props) { let ref = useRef<HTMLElement | null>(null); let { landmarkProps } = useLandmark({ ...props, role: 'navigation' }, ref); return ( <nav ref={ref} {...props} {...landmarkProps}> {props.children} </nav> ); } function Region(props) { let ref = useRef<HTMLElement | null>(null); let { landmarkProps } = useLandmark({ ...props, role: 'region' }, ref); return ( <article ref={ref} {...props} {...landmarkProps}> {props.children} </article> ); } function Search(props) { let ref = useRef<HTMLFormElement | null>(null); let { landmarkProps } = useLandmark({ ...props, role: 'search' }, ref); return ( <form ref={ref} {...props} {...landmarkProps}> <h2 id="search-header">Search</h2> <input aria-labelledby="search-header" type="search" /> </form> ); } <div> <Navigation> <h2>Navigation</h2> <ul> <li> <a href="#">Link 1</a> </li> <li> <a href="#">Link 2</a> </li> </ul> </Navigation> <Search /> <Region aria-label="Example region"> <h2>Region</h2> <p>Example region with no focusable children.</p> </Region> </div> import {useLandmark} from 'react-aria'; import {useRef} from 'react'; function Navigation( props ) { let ref = useRef< HTMLElement | null >(null); let { landmarkProps } = useLandmark({ ...props, role: 'navigation' }, ref); return ( <nav ref={ref} {...props} {...landmarkProps} > {props.children} </nav> ); } function Region(props) { let ref = useRef< HTMLElement | null >(null); let { landmarkProps } = useLandmark({ ...props, role: 'region' }, ref); return ( <article ref={ref} {...props} {...landmarkProps} > {props.children} </article> ); } function Search(props) { let ref = useRef< | HTMLFormElement | null >(null); let { landmarkProps } = useLandmark({ ...props, role: 'search' }, ref); return ( <form ref={ref} {...props} {...landmarkProps} > <h2 id="search-header"> Search </h2> <input aria-labelledby="search-header" type="search" /> </form> ); } <div> <Navigation> <h2>Navigation</h2> <ul> <li> <a href="#"> Link 1 </a> </li> <li> <a href="#"> Link 2 </a> </li> </ul> </Navigation> <Search /> <Region aria-label="Example region"> <h2>Region</h2> <p> Example region with no focusable children. </p> </Region> </div> ## Navigation * Link 1 * Link 2 ## Search ## Region Example region with no focusable children. --- ## Page: https://react-spectrum.adobe.com/react-aria/useLongPress.html # useLongPress Handles long press interactions across mouse and touch devices. Supports a customizable time threshold, accessibility description, and normalizes behavior across browsers and devices. <table><tbody><tr><th>install</th><td><code>yarn add react-aria</code></td></tr><tr><th>version</th><td>3.41.1</td></tr><tr><th>usage</th><td><code><span>import</span> {useLongPress} <span>from</span> <span>'react-aria'</span></code></td></tr></tbody></table> View repository GitHub View package NPM ## API# * * * `useLongPress( (props: LongPressProps )): LongPressResult ` ## Features# * * * `useLongPress` handles long press interactions across both mouse and touch devices. A long press is triggered when a user presses and holds their pointer over a target for a minimum period of time. If the user moves their pointer off of the target before the time threshold, the interaction is canceled. Once a long press event is triggered, other pointer interactions that may be active such as `usePress` and `useMove` will be canceled so that only the long press is activated. * Handles mouse and touch events * Uses pointer events where available, with fallbacks to mouse and touch events * Ignores emulated mouse events in mobile browsers * Prevents text selection on touch devices while long pressing * Prevents browser and OS context menus from appearing while long pressing * Customizable time threshold for long press * Supports an accessibility description to indicate to assistive technology users that a long press action is available ## Usage# * * * `useLongPress` returns props that you should spread onto the target element: | Name | Type | Description | | --- | --- | --- | | `longPressProps` | `DOMAttributes` | Props to spread on the target element. | `useLongPress` supports the following event handlers and options: | Name | Type | Default | Description | | --- | --- | --- | --- | | `isDisabled` | `boolean` | — | Whether long press events should be disabled. | | `onLongPressStart` | `( (e: LongPressEvent )) => void` | — | Handler that is called when a long press interaction starts. | | `onLongPressEnd` | `( (e: LongPressEvent )) => void` | — | Handler that is called when a long press interaction ends, either over the target or when the pointer leaves the target. | | `onLongPress` | `( (e: LongPressEvent )) => void` | — | Handler that is called when the threshold time is met while the press is over the target. | | `threshold` | `number` | `500ms` | The amount of time in milliseconds to wait before triggering a long press. | | `accessibilityDescription` | `string` | — | A description for assistive techology users indicating that a long press action is available, e.g. "Long press to open menu". | Each of these handlers is fired with a `LongPressEvent`, which exposes information about the target and the type of event that triggered the interaction. | Name | Type | Description | | --- | --- | --- | | `type` | `'longpressstart' | 'longpressend' | 'longpress'` | The type of long press event being fired. | | `pointerType` | ` PointerType ` | The pointer type that triggered the press event. | | `target` | `Element` | The target element of the press event. | | `shiftKey` | `boolean` | Whether the shift keyboard modifier was held during the press event. | | `ctrlKey` | `boolean` | Whether the ctrl keyboard modifier was held during the press event. | | `metaKey` | `boolean` | Whether the meta keyboard modifier was held during the press event. | | `altKey` | `boolean` | Whether the alt keyboard modifier was held during the press event. | | `x` | `number` | X position relative to the target. | | `y` | `number` | Y position relative to the target. | ## Example# * * * This example shows a button that has both a normal press action using usePress, as well as a long press action using `useLongPress`. Pressing the button will set the mode to "Normal speed", and long pressing it will set the mode to "Hyper speed". All of the emitted events are also logged below. Note that when long pressing the button, only a long press is emitted, and no normal press is emitted on pointer up. **Note**: this example does not have a keyboard accessible way to trigger the long press action. Because the method of triggering this action will differ depending on the component, it is outside the scope of `useLongPress`. Make sure to implement a keyboard friendly alternative to all long press interactions if you are using this hook directly. import {mergeProps, useLongPress, usePress} from 'react-aria'; function Example() { let [events, setEvents] = React.useState([]); let [mode, setMode] = React.useState('Activate'); let { longPressProps } = useLongPress({ accessibilityDescription: 'Long press to activate hyper speed', onLongPressStart: (e) => setEvents( (events) => [`long press start with ${e.pointerType}`, ...events] ), onLongPressEnd: (e) => setEvents( (events) => [`long press end with ${e.pointerType}`, ...events] ), onLongPress: (e) => { setMode('Hyper speed'); setEvents( (events) => [`long press with ${e.pointerType}`, ...events] ); } }); let { pressProps } = usePress({ onPress: (e) => { setMode('Normal speed'); setEvents( (events) => [`press with ${e.pointerType}`, ...events] ); } }); return ( <> <button {...mergeProps(pressProps, longPressProps)}>{mode}</button> <ul style={{ maxHeight: '200px', overflow: 'auto' }} > {events.map((e, i) => <li key={i}>{e}</li>)} </ul> </> ); } import { mergeProps, useLongPress, usePress } from 'react-aria'; function Example() { let [events, setEvents] = React.useState([]); let [mode, setMode] = React.useState('Activate'); let { longPressProps } = useLongPress({ accessibilityDescription: 'Long press to activate hyper speed', onLongPressStart: (e) => setEvents( (events) => [ `long press start with ${e.pointerType}`, ...events ] ), onLongPressEnd: (e) => setEvents( (events) => [ `long press end with ${e.pointerType}`, ...events ] ), onLongPress: (e) => { setMode('Hyper speed'); setEvents( (events) => [ `long press with ${e.pointerType}`, ...events ] ); } }); let { pressProps } = usePress({ onPress: (e) => { setMode('Normal speed'); setEvents( (events) => [ `press with ${e.pointerType}`, ...events ] ); } }); return ( <> <button {...mergeProps(pressProps, longPressProps)}> {mode} </button> <ul style={{ maxHeight: '200px', overflow: 'auto' }} > {events.map((e, i) => <li key={i}>{e}</li>)} </ul> </> ); } import { mergeProps, useLongPress, usePress } from 'react-aria'; function Example() { let [ events, setEvents ] = React.useState([]); let [mode, setMode] = React.useState( 'Activate' ); let { longPressProps } = useLongPress({ accessibilityDescription: 'Long press to activate hyper speed', onLongPressStart: (e) => setEvents( (events) => [ `long press start with ${e.pointerType}`, ...events ] ), onLongPressEnd: (e) => setEvents( (events) => [ `long press end with ${e.pointerType}`, ...events ] ), onLongPress: (e) => { setMode( 'Hyper speed' ); setEvents( (events) => [ `long press with ${e.pointerType}`, ...events ] ); } }); let { pressProps } = usePress({ onPress: (e) => { setMode( 'Normal speed' ); setEvents( (events) => [ `press with ${e.pointerType}`, ...events ] ); } }); return ( <> <button {...mergeProps( pressProps, longPressProps )} > {mode} </button> <ul style={{ maxHeight: '200px', overflow: 'auto' }} > {events.map(( e, i ) => ( <li key={i}> {e} </li> ))} </ul> </> ); } Activate --- ## Page: https://react-spectrum.adobe.com/react-aria/useMove.html # useMove Handles move interactions across mouse, touch, and keyboard, including dragging with the mouse or touch, and using the arrow keys. Normalizes behavior across browsers and platforms, and ignores emulated mouse events on touch devices. <table><tbody><tr><th>install</th><td><code>yarn add react-aria</code></td></tr><tr><th>version</th><td>3.41.1</td></tr><tr><th>usage</th><td><code><span>import</span> {useMove} <span>from</span> <span>'react-aria'</span></code></td></tr></tbody></table> View repository GitHub View package NPM ## API# * * * `useMove( (props: MoveEvents )): MoveResult ` ## Features# * * * `useMove` handles move interactions across mouse, touch, and keyboard. A move interaction starts when a user moves after pressing down with a mouse or their finger on the target, and ends when they lift their pointer. Move events are fired as the pointer moves around, and specify the distance that the pointer traveled since the last event. In addition, after a user focuses the target element, move events are fired when the user presses the arrow keys. * Handles mouse and touch events * Handles arrow key presses * Uses pointer events where available, with fallbacks to mouse and touch events * Ignores emulated mouse events in mobile browsers * Handles disabling text selection on mobile while the press interaction is active * Normalizes many cross browser inconsistencies ## Usage# * * * `useMove` returns props that you should spread onto the target element: | Name | Type | Description | | --- | --- | --- | | `moveProps` | `DOMAttributes` | Props to spread on the target element. | `useMove` supports the following event handlers: | Name | Type | Description | | --- | --- | --- | | `onMoveStart` | `( (e: MoveStartEvent )) => void` | Handler that is called when a move interaction starts. | | `onMove` | `( (e: MoveMoveEvent )) => void` | Handler that is called when the element is moved. | | `onMoveEnd` | `( (e: MoveEndEvent )) => void` | Handler that is called when a move interaction ends. | Each of these handlers is fired with a `MoveEvent`, which exposes information about the target and the type of event that triggered the interaction. | Name | Type | Description | | --- | --- | --- | | `type` | `'move'` | The type of move event being fired. | | `deltaX` | `number` | The amount moved in the X direction since the last event. | | `deltaY` | `number` | The amount moved in the Y direction since the last event. | | `pointerType` | ` PointerType ` | The pointer type that triggered the move event. | | `shiftKey` | `boolean` | Whether the shift keyboard modifier was held during the move event. | | `ctrlKey` | `boolean` | Whether the ctrl keyboard modifier was held during the move event. | | `metaKey` | `boolean` | Whether the meta keyboard modifier was held during the move event. | | `altKey` | `boolean` | Whether the alt keyboard modifier was held during the move event. | ## Example# * * * This example shows a ball that can be moved by dragging with a mouse or touch, or by tabbing to it and using the arrow keys on your keyboard. The movement is clamped so that the ball cannot be dragged outside a box. In addition, all of the move events are logged below so that you can inspect what is going on. import {useMove} from 'react-aria'; function Example() { const CONTAINER_SIZE = 200; const BALL_SIZE = 30; let [events, setEvents] = React.useState([]); let [color, setColor] = React.useState('black'); let [position, setPosition] = React.useState({ x: 0, y: 0 }); let clamp = (pos) => Math.min(Math.max(pos, 0), CONTAINER_SIZE - BALL_SIZE); let { moveProps } = useMove({ onMoveStart(e) { setColor('red'); setEvents( (events) => [ `move start with pointerType = ${e.pointerType}`, ...events ] ); }, onMove(e) { setPosition(({ x, y }) => { // Normally, we want to allow the user to continue // dragging outside the box such that they need to // drag back over the ball again before it moves. // This is handled below by clamping during render. // If using the keyboard, however, we need to clamp // here so that dragging outside the container and // then using the arrow keys works as expected. if (e.pointerType === 'keyboard') { x = clamp(x); y = clamp(y); } x += e.deltaX; y += e.deltaY; return { x, y }; }); setEvents( (events) => [ `move with pointerType = ${e.pointerType}, deltaX = ${e.deltaX}, deltaY = ${e.deltaY}`, ...events ] ); }, onMoveEnd(e) { setPosition(({ x, y }) => { // Clamp position on mouse up x = clamp(x); y = clamp(y); return { x, y }; }); setColor('black'); setEvents( (events) => [`move end with pointerType = ${e.pointerType}`, ...events] ); } }); return ( <> <div style={{ width: CONTAINER_SIZE, height: CONTAINER_SIZE, background: 'white', border: '1px solid black', position: 'relative', touchAction: 'none' }} > <div {...moveProps} tabIndex={0} style={{ width: BALL_SIZE, height: BALL_SIZE, borderRadius: '100%', position: 'absolute', left: clamp(position.x), top: clamp(position.y), background: color }} /> </div> <ul style={{ maxHeight: '200px', overflow: 'auto' }} > {events.map((e, i) => <li key={i}>{e}</li>)} </ul> </> ); } import {useMove} from 'react-aria'; function Example() { const CONTAINER_SIZE = 200; const BALL_SIZE = 30; let [events, setEvents] = React.useState([]); let [color, setColor] = React.useState('black'); let [position, setPosition] = React.useState({ x: 0, y: 0 }); let clamp = (pos) => Math.min(Math.max(pos, 0), CONTAINER_SIZE - BALL_SIZE); let { moveProps } = useMove({ onMoveStart(e) { setColor('red'); setEvents( (events) => [ `move start with pointerType = ${e.pointerType}`, ...events ] ); }, onMove(e) { setPosition(({ x, y }) => { // Normally, we want to allow the user to continue // dragging outside the box such that they need to // drag back over the ball again before it moves. // This is handled below by clamping during render. // If using the keyboard, however, we need to clamp // here so that dragging outside the container and // then using the arrow keys works as expected. if (e.pointerType === 'keyboard') { x = clamp(x); y = clamp(y); } x += e.deltaX; y += e.deltaY; return { x, y }; }); setEvents( (events) => [ `move with pointerType = ${e.pointerType}, deltaX = ${e.deltaX}, deltaY = ${e.deltaY}`, ...events ] ); }, onMoveEnd(e) { setPosition(({ x, y }) => { // Clamp position on mouse up x = clamp(x); y = clamp(y); return { x, y }; }); setColor('black'); setEvents( (events) => [ `move end with pointerType = ${e.pointerType}`, ...events ] ); } }); return ( <> <div style={{ width: CONTAINER_SIZE, height: CONTAINER_SIZE, background: 'white', border: '1px solid black', position: 'relative', touchAction: 'none' }} > <div {...moveProps} tabIndex={0} style={{ width: BALL_SIZE, height: BALL_SIZE, borderRadius: '100%', position: 'absolute', left: clamp(position.x), top: clamp(position.y), background: color }} /> </div> <ul style={{ maxHeight: '200px', overflow: 'auto' }} > {events.map((e, i) => <li key={i}>{e}</li>)} </ul> </> ); } import {useMove} from 'react-aria'; function Example() { const CONTAINER_SIZE = 200; const BALL_SIZE = 30; let [ events, setEvents ] = React.useState([]); let [color, setColor] = React.useState( 'black' ); let [ position, setPosition ] = React.useState({ x: 0, y: 0 }); let clamp = (pos) => Math.min( Math.max(pos, 0), CONTAINER_SIZE - BALL_SIZE ); let { moveProps } = useMove({ onMoveStart(e) { setColor('red'); setEvents( (events) => [ `move start with pointerType = ${e.pointerType}`, ...events ] ); }, onMove(e) { setPosition( ({ x, y }) => { // Normally, we want to allow the user to continue // dragging outside the box such that they need to // drag back over the ball again before it moves. // This is handled below by clamping during render. // If using the keyboard, however, we need to clamp // here so that dragging outside the container and // then using the arrow keys works as expected. if ( e.pointerType === 'keyboard' ) { x = clamp( x ); y = clamp( y ); } x += e.deltaX; y += e.deltaY; return { x, y }; } ); setEvents( (events) => [ `move with pointerType = ${e.pointerType}, deltaX = ${e.deltaX}, deltaY = ${e.deltaY}`, ...events ] ); }, onMoveEnd(e) { setPosition( ({ x, y }) => { // Clamp position on mouse up x = clamp(x); y = clamp(y); return { x, y }; } ); setColor( 'black' ); setEvents( (events) => [ `move end with pointerType = ${e.pointerType}`, ...events ] ); } }); return ( <> <div style={{ width: CONTAINER_SIZE, height: CONTAINER_SIZE, background: 'white', border: '1px solid black', position: 'relative', touchAction: 'none' }} > <div {...moveProps} tabIndex={0} style={{ width: BALL_SIZE, height: BALL_SIZE, borderRadius: '100%', position: 'absolute', left: clamp( position.x ), top: clamp( position.y ), background: color }} /> </div> <ul style={{ maxHeight: '200px', overflow: 'auto' }} > {events.map(( e, i ) => ( <li key={i}> {e} </li> ))} </ul> </> ); } --- ## Page: https://react-spectrum.adobe.com/react-aria/usePress.html # usePress Handles press interactions across mouse, touch, keyboard, and screen readers. It normalizes behavior across browsers and platforms, and handles many nuances of dealing with pointer and keyboard events. <table><tbody><tr><th>install</th><td><code>yarn add react-aria</code></td></tr><tr><th>version</th><td>3.41.1</td></tr><tr><th>usage</th><td><code><span>import</span> {usePress} <span>from</span> <span>'react-aria'</span></code></td></tr></tbody></table> View repository GitHub View package NPM ## API# * * * `usePress( (props: PressHookProps )): PressResult ` ## Features# * * * `usePress` handles press interactions across mouse, touch, keyboard, and screen readers. A press interaction starts when a user presses down with a mouse or their finger on the target, and ends when they move the pointer off the target. It may start again if the pointer re-enters the target. `usePress` returns the current press state, which can be used to adjust the visual appearance of the target. If the pointer is released over the target, then an `onPress` event is fired. * Handles mouse and touch events * Handles Enter or Space key presses * Handles screen reader virtual clicks * Uses pointer events where available, with fallbacks to mouse and touch events * Normalizes focus behavior on mouse and touch interactions across browsers * Handles disabling text selection on mobile while the press interaction is active * Handles canceling press interactions on scroll * Normalizes many cross browser inconsistencies Read our blog post about the complexities of press event handling to learn more. ## Usage# * * * `usePress` returns props that you should spread onto the target element, along with the current press state: | Name | Type | Description | | --- | --- | --- | | `isPressed` | `boolean` | Whether the target is currently pressed. | | `pressProps` | `DOMAttributes` | Props to spread on the target element. | `usePress` supports the following event handlers: | Name | Type | Description | | --- | --- | --- | | `onPress` | `( (e: PressEvent )) => void` | Handler that is called when the press is released over the target. | | `onPressStart` | `( (e: PressEvent )) => void` | Handler that is called when a press interaction starts. | | `onPressEnd` | `( (e: PressEvent )) => void` | Handler that is called when a press interaction ends, either over the target or when the pointer leaves the target. | | `onPressChange` | `( (isPressed: boolean )) => void` | Handler that is called when the press state changes. | | `onPressUp` | `( (e: PressEvent )) => void` | Handler that is called when a press is released over the target, regardless of whether it started on the target or not. | | `onClick` | `( (e: MouseEvent<FocusableElement> )) => void` | **Not recommended – use `onPress` instead.** `onClick` is an alias for `onPress` provided for compatibility with other libraries. `onPress` provides additional event details for non-mouse interactions. | Each of these handlers is fired with a `PressEvent`, which exposes information about the target and the type of event that triggered the interaction. ### Properties | Name | Type | Description | | --- | --- | --- | | `type` | `'pressstart' | 'pressend' | 'pressup' | 'press'` | The type of press event being fired. | | `pointerType` | ` PointerType ` | The pointer type that triggered the press event. | | `target` | `Element` | The target element of the press event. | | `shiftKey` | `boolean` | Whether the shift keyboard modifier was held during the press event. | | `ctrlKey` | `boolean` | Whether the ctrl keyboard modifier was held during the press event. | | `metaKey` | `boolean` | Whether the meta keyboard modifier was held during the press event. | | `altKey` | `boolean` | Whether the alt keyboard modifier was held during the press event. | | `x` | `number` | X position relative to the target. | | `y` | `number` | Y position relative to the target. | ### Methods | Method | Description | | --- | --- | | `continuePropagation(): void` | By default, press events stop propagation to parent elements. In cases where a handler decides not to handle a specific event, it can call `continuePropagation()` to allow a parent to handle it. | ## Example# * * * This example shows a simple target that handles press events with `usePress` and logs them to a list below. It also uses the `isPressed` state to adjust the background color when the target is pressed. Press down on the target and drag your pointer off and over to see when the events are fired, and try focusing the target with a keyboard and pressing the Enter or Space keys to trigger events as well. **NOTE: for more advanced button functionality, see useButton.** import {usePress} from 'react-aria'; function Example() { let [events, setEvents] = React.useState([]); let { pressProps, isPressed } = usePress({ onPressStart: (e) => setEvents( (events) => [...events, `press start with ${e.pointerType}`] ), onPressEnd: (e) => setEvents( (events) => [...events, `press end with ${e.pointerType}`] ), onPress: (e) => setEvents( (events) => [...events, `press with ${e.pointerType}`] ) }); return ( <> <div {...pressProps} style={{ background: isPressed ? 'darkgreen' : 'green', color: 'white', display: 'inline-block', padding: 4, cursor: 'pointer' }} role="button" tabIndex={0} > Press me! </div> <ul style={{ maxHeight: '200px', overflow: 'auto' }} > {events.map((e, i) => <li key={i}>{e}</li>)} </ul> </> ); } import {usePress} from 'react-aria'; function Example() { let [events, setEvents] = React.useState([]); let { pressProps, isPressed } = usePress({ onPressStart: (e) => setEvents( (events) => [ ...events, `press start with ${e.pointerType}` ] ), onPressEnd: (e) => setEvents( (events) => [ ...events, `press end with ${e.pointerType}` ] ), onPress: (e) => setEvents( (events) => [ ...events, `press with ${e.pointerType}` ] ) }); return ( <> <div {...pressProps} style={{ background: isPressed ? 'darkgreen' : 'green', color: 'white', display: 'inline-block', padding: 4, cursor: 'pointer' }} role="button" tabIndex={0} > Press me! </div> <ul style={{ maxHeight: '200px', overflow: 'auto' }} > {events.map((e, i) => <li key={i}>{e}</li>)} </ul> </> ); } import {usePress} from 'react-aria'; function Example() { let [ events, setEvents ] = React.useState([]); let { pressProps, isPressed } = usePress({ onPressStart: (e) => setEvents( (events) => [ ...events, `press start with ${e.pointerType}` ] ), onPressEnd: (e) => setEvents( (events) => [ ...events, `press end with ${e.pointerType}` ] ), onPress: (e) => setEvents( (events) => [ ...events, `press with ${e.pointerType}` ] ) }); return ( <> <div {...pressProps} style={{ background: isPressed ? 'darkgreen' : 'green', color: 'white', display: 'inline-block', padding: 4, cursor: 'pointer' }} role="button" tabIndex={0} > Press me! </div> <ul style={{ maxHeight: '200px', overflow: 'auto' }} > {events.map(( e, i ) => ( <li key={i}> {e} </li> ))} </ul> </> ); } Press me! --- ## Page: https://react-spectrum.adobe.com/react-aria/useClipboard.html # useClipboard Handles clipboard interactions for a focusable element. Supports items of multiple data types, and integrates with the operating system native clipboard. <table><tbody><tr><th>install</th><td><code>yarn add react-aria</code></td></tr><tr><th>version</th><td>3.41.1</td></tr><tr><th>usage</th><td><code><span>import</span> {useClipboard} <span>from</span> <span>'react-aria'</span></code></td></tr></tbody></table> View repository GitHub View package NPM ## API# * * * `useClipboard( (options: ClipboardProps )): ClipboardResult ` ## Introduction# * * * Copy and paste is a common way to transfer data between locations, either within or between apps. Browsers support copy and paste of selected text content by default, but rich objects with custom data can also be copied and pasted using the clipboard events API. For example, an app could support copying and pasting a selected card representing a rich object to a new location, or allow a user to paste files from their device to upload them. This can provide a keyboard accessible alternative to drag and drop. The `useClipboard` hook provides a simple way to implement copy and paste for a focusable element. When focused, users can press keyboard shortcuts like ⌘C and ⌘V, or even use the browser's "Copy" and "Paste" menu commands, to trigger clipboard events. Multiple items can be copied and pasted at once, each represented in one or more different data formats. Because it uses native browser APIs under the hood, copy and paste uses the operating system clipboard, which means it works between applications (e.g. Finder, Windows Explorer, a native email app, etc.) in addition to within the app. ## Example# * * * This example shows a simple focusable element which supports copying a string when focused, and another element which supports pasting plain text. import type {TextDropItem} from 'react-aria'; import {useClipboard} from 'react-aria'; function Copyable() { let { clipboardProps } = useClipboard({ getItems() { return [{ 'text/plain': 'Hello world' }]; } }); return ( <div role="textbox" tabIndex={0} {...clipboardProps}> Hello world <kbd>⌘C</kbd> </div> ); } function Pasteable() { let [pasted, setPasted] = React.useState(null); let { clipboardProps } = useClipboard({ async onPaste(items) { let pasted = await Promise.all( items .filter((item) => item.kind === 'text' && item.types.has('text/plain') ) .map((item: TextDropItem) => item.getText('text/plain')) ); setPasted(pasted.join('\n')); } }); return ( <div role="textbox" tabIndex={0} {...clipboardProps}> {pasted || 'Paste here'} <kbd>⌘V</kbd> </div> ); } <Copyable /> <Pasteable /> import type {TextDropItem} from 'react-aria'; import {useClipboard} from 'react-aria'; function Copyable() { let { clipboardProps } = useClipboard({ getItems() { return [{ 'text/plain': 'Hello world' }]; } }); return ( <div role="textbox" tabIndex={0} {...clipboardProps}> Hello world <kbd>⌘C</kbd> </div> ); } function Pasteable() { let [pasted, setPasted] = React.useState(null); let { clipboardProps } = useClipboard({ async onPaste(items) { let pasted = await Promise.all( items .filter((item) => item.kind === 'text' && item.types.has('text/plain') ) .map((item: TextDropItem) => item.getText('text/plain') ) ); setPasted(pasted.join('\n')); } }); return ( <div role="textbox" tabIndex={0} {...clipboardProps}> {pasted || 'Paste here'} <kbd>⌘V</kbd> </div> ); } <Copyable /> <Pasteable /> import type {TextDropItem} from 'react-aria'; import {useClipboard} from 'react-aria'; function Copyable() { let { clipboardProps } = useClipboard({ getItems() { return [{ 'text/plain': 'Hello world' }]; } }); return ( <div role="textbox" tabIndex={0} {...clipboardProps} > Hello world <kbd>⌘C</kbd> </div> ); } function Pasteable() { let [ pasted, setPasted ] = React.useState( null ); let { clipboardProps } = useClipboard({ async onPaste( items ) { let pasted = await Promise .all( items .filter(( item ) => item .kind === 'text' && item .types .has( 'text/plain' ) ) .map(( item: TextDropItem ) => item .getText( 'text/plain' ) ) ); setPasted( pasted.join('\n') ); } }); return ( <div role="textbox" tabIndex={0} {...clipboardProps} > {pasted || 'Paste here'} <kbd>⌘V</kbd> </div> ); } <Copyable /> <Pasteable /> Hello world⌘C Paste here⌘V Show CSS [role=textbox] { display: inline-flex; align-items: center; justify-content: center; background: var(--spectrum-global-color-gray-50); border: 1px solid var(--spectrum-global-color-gray-400); padding: 10px; margin-right: 20px; border-radius: 8px; } [role=textbox]:focus { outline: none; border-color: var(--blue); box-shadow: 0 0 0 1px var(--blue); } [role=textbox] kbd { display: inline-block; margin-left: 10px; padding: 0 4px; background: var(--spectrum-global-color-gray-100); border: 1px solid var(--spectrum-global-color-gray-300); border-radius: 4px; font-size: small; letter-spacing: .2em; } [role=textbox] { display: inline-flex; align-items: center; justify-content: center; background: var(--spectrum-global-color-gray-50); border: 1px solid var(--spectrum-global-color-gray-400); padding: 10px; margin-right: 20px; border-radius: 8px; } [role=textbox]:focus { outline: none; border-color: var(--blue); box-shadow: 0 0 0 1px var(--blue); } [role=textbox] kbd { display: inline-block; margin-left: 10px; padding: 0 4px; background: var(--spectrum-global-color-gray-100); border: 1px solid var(--spectrum-global-color-gray-300); border-radius: 4px; font-size: small; letter-spacing: .2em; } [role=textbox] { display: inline-flex; align-items: center; justify-content: center; background: var(--spectrum-global-color-gray-50); border: 1px solid var(--spectrum-global-color-gray-400); padding: 10px; margin-right: 20px; border-radius: 8px; } [role=textbox]:focus { outline: none; border-color: var(--blue); box-shadow: 0 0 0 1px var(--blue); } [role=textbox] kbd { display: inline-block; margin-left: 10px; padding: 0 4px; background: var(--spectrum-global-color-gray-100); border: 1px solid var(--spectrum-global-color-gray-300); border-radius: 4px; font-size: small; letter-spacing: .2em; } ## Copy data# * * * Data to copy can be provided in multiple formats at once. This allows the destination where the user pastes to choose the data that it understands. For example, you could serialize a complex object as JSON in a custom format for use within your own application, and also provide plain text and/or rich HTML fallbacks that can be used when a user pastes in an external application (e.g. an email message). This can be done by returning multiple keys for an item from the `getItems` function. Types can either be a standard mime type for interoperability with external applications, or a custom string for use within your own app. In addition to providing items in multiple formats, you can also return multiple drag items from `getItems` to transfer multiple objects in a single copy and paste operation. This example copies two items, each of which contains representations as plain text, HTML, and a custom app-specific data format. Pasting on the target will use the custom data format to render formatted items. If you paste in an external application supporting rich text, the HTML representation will be used. Dropping in a text editor will use the plain text format. function Copyable() { let {clipboardProps} = useClipboard({ getItems() { return [{ 'text/plain': 'hello world', 'text/html': '<strong>hello world</strong>', 'my-app-custom-type': JSON.stringify({ message: 'hello world', style: 'bold' }) }, { 'text/plain': 'foo bar', 'text/html': '<em>foo bar</em>', 'my-app-custom-type': JSON.stringify({ message: 'foo bar', style: 'italic' }) }]; } }); return ( <div role="textbox" tabIndex={0} {...clipboardProps}> <div> <div><strong>hello world</strong></div> <div><em>foo bar</em></div> </div> <kbd>⌘C</kbd> </div> ); } function Copyable() { let {clipboardProps} = useClipboard({ getItems() { return [{ 'text/plain': 'hello world', 'text/html': '<strong>hello world</strong>', 'my-app-custom-type': JSON.stringify({ message: 'hello world', style: 'bold' }) }, { 'text/plain': 'foo bar', 'text/html': '<em>foo bar</em>', 'my-app-custom-type': JSON.stringify({ message: 'foo bar', style: 'italic' }) }]; } }); return ( <div role="textbox" tabIndex={0} {...clipboardProps}> <div> <div><strong>hello world</strong></div> <div><em>foo bar</em></div> </div> <kbd>⌘C</kbd> </div> ); } function Copyable() { let { clipboardProps } = useClipboard({ getItems() { return [{ 'text/plain': 'hello world', 'text/html': '<strong>hello world</strong>', 'my-app-custom-type': JSON.stringify( { message: 'hello world', style: 'bold' } ) }, { 'text/plain': 'foo bar', 'text/html': '<em>foo bar</em>', 'my-app-custom-type': JSON.stringify( { message: 'foo bar', style: 'italic' } ) }]; } }); return ( <div role="textbox" tabIndex={0} {...clipboardProps} > <div> <div> <strong> hello world </strong> </div> <div> <em> foo bar </em> </div> </div> <kbd>⌘C</kbd> </div> ); } **hello world** _foo bar_ ⌘C Paste here ⌘V ## Paste data# * * * `useClipboard` allows users to paste one or more items, each of which contains data to be pasted. There are three kinds of items: * `text` – represents data inline as a string in one or more formats * `file` – references a file on the user's device * `directory` – references the contents of a directory ### Text# A `TextDropItem` represents textual data in one or more different formats. These may be either standard mime types or custom app-specific formats. Representing data in multiple formats allows drop targets both within and outside an application to choose data in a format that they understand. For example, a complex object may be serialized in a custom format for use within an application, with fallbacks in plain text and/or rich HTML that can be used when a user drops data from an external application. The example below works with the above `Copyable` example using a custom app-specific data format to transfer rich data. If no such data is available, it falls back to pasting plain text data. function Pasteable() { let [pasted, setPasted] = React.useState(null); let { clipboardProps } = useClipboard({ async onPaste(items) { let pasted = await Promise.all( items .filter((item) => item.kind === 'text' && (item.types.has('text/plain') || item.types.has('my-app-custom-type')) ) .map(async (item: TextDropItem) => { if (item.types.has('my-app-custom-type')) { return JSON.parse(await item.getText('my-app-custom-type')); } else { return { message: await item.getText('text/plain') }; } }) ); setPasted(pasted); } }); let message = ['Paste here']; if (pasted) { message = pasted.map((d) => { let message = d.message; if (d.style === 'bold') { message = <strong>{message}</strong>; } else if (d.style === 'italic') { message = <em>{message}</em>; } return <div>{message}</div>; }); } return ( <div role="textbox" tabIndex={0} {...clipboardProps}> <div>{message || 'Paste here'}</div> <kbd>⌘V</kbd> </div> ); } function Pasteable() { let [pasted, setPasted] = React.useState(null); let { clipboardProps } = useClipboard({ async onPaste(items) { let pasted = await Promise.all( items .filter((item) => item.kind === 'text' && (item.types.has('text/plain') || item.types.has('my-app-custom-type')) ) .map(async (item: TextDropItem) => { if (item.types.has('my-app-custom-type')) { return JSON.parse( await item.getText('my-app-custom-type') ); } else { return { message: await item.getText('text/plain') }; } }) ); setPasted(pasted); } }); let message = ['Paste here']; if (pasted) { message = pasted.map((d) => { let message = d.message; if (d.style === 'bold') { message = <strong>{message}</strong>; } else if (d.style === 'italic') { message = <em>{message}</em>; } return <div>{message}</div>; }); } return ( <div role="textbox" tabIndex={0} {...clipboardProps}> <div>{message || 'Paste here'}</div> <kbd>⌘V</kbd> </div> ); } function Pasteable() { let [ pasted, setPasted ] = React.useState( null ); let { clipboardProps } = useClipboard({ async onPaste( items ) { let pasted = await Promise .all( items .filter( (item) => item .kind === 'text' && (item .types .has( 'text/plain' ) || item .types .has( 'my-app-custom-type' )) ) .map( async ( item: TextDropItem ) => { if ( item .types .has( 'my-app-custom-type' ) ) { return JSON .parse( await item .getText( 'my-app-custom-type' ) ); } else { return { message: await item .getText( 'text/plain' ) }; } } ) ); setPasted(pasted); } }); let message = [ 'Paste here' ]; if (pasted) { message = pasted.map( (d) => { let message = d.message; if ( d.style === 'bold' ) { message = ( <strong> {message} </strong> ); } else if ( d.style === 'italic' ) { message = ( <em> {message} </em> ); } return ( <div> {message} </div> ); } ); } return ( <div role="textbox" tabIndex={0} {...clipboardProps} > <div> {message || 'Paste here'} </div> <kbd>⌘V</kbd> </div> ); } **hello world** _foo bar_ ⌘C Paste here ⌘V ### Files# A `FileDropItem` references a file on the user's device. It includes the name and mime type of the file, and methods to read the contents as plain text, or retrieve a native File object which can be attached to form data for uploading. This example accepts JPEG and PNG image files, and renders them by creating a local object URL. import type {FileDropItem} from 'react-aria'; function Pasteable() { let [file, setFile] = React.useState(null); let { clipboardProps } = useClipboard({ async onPaste(items) { let item = items.find((item) => item.kind === 'file' && (item.type === 'image/jpeg' || item.type === 'image/png') ) as FileDropItem; if (item) { setFile(URL.createObjectURL(await item.getFile())); } } }); return ( <div role="textbox" tabIndex={0} {...clipboardProps} style={{ width: 150, height: 100 }} > {file ? ( <img src={file} style={{ width: '100%', height: '100%', objectFit: 'contain' }} /> ) : 'Paste image here'} </div> ); } import type {FileDropItem} from 'react-aria'; function Pasteable() { let [file, setFile] = React.useState(null); let { clipboardProps } = useClipboard({ async onPaste(items) { let item = items.find((item) => item.kind === 'file' && (item.type === 'image/jpeg' || item.type === 'image/png') ) as FileDropItem; if (item) { setFile(URL.createObjectURL(await item.getFile())); } } }); return ( <div role="textbox" tabIndex={0} {...clipboardProps} style={{ width: 150, height: 100 }} > {file ? ( <img src={file} style={{ width: '100%', height: '100%', objectFit: 'contain' }} /> ) : 'Paste image here'} </div> ); } import type {FileDropItem} from 'react-aria'; function Pasteable() { let [file, setFile] = React.useState(null); let { clipboardProps } = useClipboard({ async onPaste( items ) { let item = items .find((item) => item.kind === 'file' && (item.type === 'image/jpeg' || item.type === 'image/png') ) as FileDropItem; if (item) { setFile( URL .createObjectURL( await item .getFile() ) ); } } }); return ( <div role="textbox" tabIndex={0} {...clipboardProps} style={{ width: 150, height: 100 }} > {file ? ( <img src={file} style={{ width: '100%', height: '100%', objectFit: 'contain' }} /> ) : 'Paste image here'} </div> ); } Paste image here ### Directories# A `DirectoryDropItem` references the contents of a directory on the user's device. It includes the name of the directory, as well as a method to iterate through the files and folders within the directory. The contents of any folders within the directory can be accessed recursively. The `getEntries` method returns an async iterable object, which can be used in a `for await...of` loop. This provides each item in the directory as either a `FileDropItem` or` DirectoryDropItem `, and you can access the contents of each file as discussed above. This example renders the file names within a dropped directory in a grid. import type {DirectoryDropItem} from 'react-aria'; import File from '@spectrum-icons/workflow/FileTxt'; import Folder from '@spectrum-icons/workflow/Folder'; function Pasteable() { let [files, setFiles] = React.useState(null); let { clipboardProps } = useClipboard({ async onPaste(items) { // Find the first dropped item that is a directory. let dir = items.find((item) => item.kind === 'directory' ) as DirectoryDropItem; if (dir) { // Read entries in directory and update state with relevant info. let files = []; for await (let entry of dir.getEntries()) { files.push({ name: entry.name, kind: entry.kind }); } setFiles(files); } } }); let contents = <>Paste directory here</>; if (files) { contents = ( <ul> {files.map((f) => ( <li key={f.name}> {f.kind === 'directory' ? <Folder /> : <File />} <span>{f.name}</span> </li> ))} </ul> ); } return ( <div role="textbox" tabIndex={0} {...clipboardProps} className="grid"> {contents} </div> ); } import type {DirectoryDropItem} from 'react-aria'; import File from '@spectrum-icons/workflow/FileTxt'; import Folder from '@spectrum-icons/workflow/Folder'; function Pasteable() { let [files, setFiles] = React.useState(null); let { clipboardProps } = useClipboard({ async onPaste(items) { // Find the first dropped item that is a directory. let dir = items.find((item) => item.kind === 'directory' ) as DirectoryDropItem; if (dir) { // Read entries in directory and update state with relevant info. let files = []; for await (let entry of dir.getEntries()) { files.push({ name: entry.name, kind: entry.kind }); } setFiles(files); } } }); let contents = <>Paste directory here</>; if (files) { contents = ( <ul> {files.map((f) => ( <li key={f.name}> {f.kind === 'directory' ? <Folder /> : <File />} <span>{f.name}</span> </li> ))} </ul> ); } return ( <div role="textbox" tabIndex={0} {...clipboardProps} className="grid" > {contents} </div> ); } import type {DirectoryDropItem} from 'react-aria'; import File from '@spectrum-icons/workflow/FileTxt'; import Folder from '@spectrum-icons/workflow/Folder'; function Pasteable() { let [files, setFiles] = React.useState(null); let { clipboardProps } = useClipboard({ async onPaste( items ) { // Find the first dropped item that is a directory. let dir = items .find((item) => item.kind === 'directory' ) as DirectoryDropItem; if (dir) { // Read entries in directory and update state with relevant info. let files = []; for await ( let entry of dir .getEntries() ) { files.push({ name: entry.name, kind: entry.kind }); } setFiles(files); } } }); let contents = ( <> Paste directory here </> ); if (files) { contents = ( <ul> {files.map( (f) => ( <li key={f .name} > {f.kind === 'directory' ? ( <Folder /> ) : ( <File /> )} <span> {f.name} </span> </li> ) )} </ul> ); } return ( <div role="textbox" tabIndex={0} {...clipboardProps} className="grid" > {contents} </div> ); } Paste directory here Show CSS .grid { display: block; width: auto; height: auto; min-height: 80px; } .grid ul { display: grid; grid-template-columns: repeat(auto-fit, 100px); list-style: none; margin: 0; padding: 0; gap: 20px; } .grid li { display: flex; align-items: center; gap: 8px; } .grid li svg { flex: 0 0 auto; } .grid li span { flex: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .grid { display: block; width: auto; height: auto; min-height: 80px; } .grid ul { display: grid; grid-template-columns: repeat(auto-fit, 100px); list-style: none; margin: 0; padding: 0; gap: 20px; } .grid li { display: flex; align-items: center; gap: 8px; } .grid li svg { flex: 0 0 auto; } .grid li span { flex: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .grid { display: block; width: auto; height: auto; min-height: 80px; } .grid ul { display: grid; grid-template-columns: repeat(auto-fit, 100px); list-style: none; margin: 0; padding: 0; gap: 20px; } .grid li { display: flex; align-items: center; gap: 8px; } .grid li svg { flex: 0 0 auto; } .grid li span { flex: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } ## Disabling copy and paste# * * * If you need to temporarily disable copying and pasting, you can pass the `isDisabled` option to `useClipboard`. This will prevent copying and pasting on the element until it is re-enabled. import type {TextDropItem} from 'react-aria'; import {useClipboard} from 'react-aria'; function Copyable() { let { clipboardProps } = useClipboard({ getItems() { return [{ 'text/plain': 'Hello world' }]; }, isDisabled: true }); return ( <div role="textbox" tabIndex={0} {...clipboardProps}> Hello world <kbd>⌘C</kbd> </div> ); } function Pasteable() { let [pasted, setPasted] = React.useState(null); let { clipboardProps } = useClipboard({ async onPaste(items) { let pasted = await Promise.all( items .filter((item) => item.kind === 'text' && item.types.has('text/plain') ) .map((item: TextDropItem) => item.getText('text/plain')) ); setPasted(pasted.join('\n')); }, isDisabled: true }); return ( <div role="textbox" tabIndex={0} {...clipboardProps}> {pasted || 'Paste here'} <kbd>⌘V</kbd> </div> ); } <Copyable /> <Pasteable /> import type {TextDropItem} from 'react-aria'; import {useClipboard} from 'react-aria'; function Copyable() { let { clipboardProps } = useClipboard({ getItems() { return [{ 'text/plain': 'Hello world' }]; }, isDisabled: true }); return ( <div role="textbox" tabIndex={0} {...clipboardProps}> Hello world <kbd>⌘C</kbd> </div> ); } function Pasteable() { let [pasted, setPasted] = React.useState(null); let { clipboardProps } = useClipboard({ async onPaste(items) { let pasted = await Promise.all( items .filter((item) => item.kind === 'text' && item.types.has('text/plain') ) .map((item: TextDropItem) => item.getText('text/plain') ) ); setPasted(pasted.join('\n')); }, isDisabled: true }); return ( <div role="textbox" tabIndex={0} {...clipboardProps}> {pasted || 'Paste here'} <kbd>⌘V</kbd> </div> ); } <Copyable /> <Pasteable /> import type {TextDropItem} from 'react-aria'; import {useClipboard} from 'react-aria'; function Copyable() { let { clipboardProps } = useClipboard({ getItems() { return [{ 'text/plain': 'Hello world' }]; }, isDisabled: true }); return ( <div role="textbox" tabIndex={0} {...clipboardProps} > Hello world <kbd>⌘C</kbd> </div> ); } function Pasteable() { let [ pasted, setPasted ] = React.useState( null ); let { clipboardProps } = useClipboard({ async onPaste( items ) { let pasted = await Promise .all( items .filter(( item ) => item .kind === 'text' && item .types .has( 'text/plain' ) ) .map(( item: TextDropItem ) => item .getText( 'text/plain' ) ) ); setPasted( pasted.join('\n') ); }, isDisabled: true }); return ( <div role="textbox" tabIndex={0} {...clipboardProps} > {pasted || 'Paste here'} <kbd>⌘V</kbd> </div> ); } <Copyable /> <Pasteable /> Hello world⌘C Paste here⌘V --- ## Page: https://react-spectrum.adobe.com/react-aria/useDrag.html # useDrag Handles drag interactions for an element, with support for traditional mouse and touch based drag and drop, in addition to full parity for keyboard and screen reader users. <table><tbody><tr><th>install</th><td><code>yarn add react-aria</code></td></tr><tr><th>version</th><td>3.41.1</td></tr><tr><th>usage</th><td><code><span>import</span> {useDrag} <span>from</span> <span>'react-aria'</span></code></td></tr></tbody></table> View repository GitHub View package NPM ## API# * * * `useDrag( (options: DragOptions )): DragResult ` ## Introduction# * * * Drag and drop is a common UI interaction that allows users to transfer data between two locations by directly moving a visual representation on screen. It is a flexible, efficient, and intuitive way for users to perform a variety of tasks, and is widely supported across both desktop and mobile operating systems. React Aria supports traditional mouse and touch based drag and drop, but also implements keyboard and screen reader friendly interactions. Users can press Enter on a draggable element to enter drag and drop mode. Then, they can press Tab to navigate between drop targets, and Enter to drop or Escape to cancel. Touch screen reader users can also drag by double tapping to activate drag and drop mode, swiping between drop targets, and double tapping again to drop. See the drag and drop introduction to learn more. ## Example# * * * This example shows how to make a simple draggable element that provides data as plain text. In order to support keyboard and screen reader drag interactions, the element must be focusable and have an ARIA role (in this case, `button`). While it is being dragged, it is displayed with a dimmed appearance by applying an additional CSS class. import {useDrag} from 'react-aria'; function Draggable() { let { dragProps, isDragging } = useDrag({ getItems() { return [{ 'text/plain': 'hello world' }]; } }); return ( <div {...dragProps} role="button" tabIndex={0} className={`draggable ${isDragging ? 'dragging' : ''}`} > Drag me </div> ); } <Draggable /> <DropTarget /> import {useDrag} from 'react-aria'; function Draggable() { let { dragProps, isDragging } = useDrag({ getItems() { return [{ 'text/plain': 'hello world' }]; } }); return ( <div {...dragProps} role="button" tabIndex={0} className={`draggable ${ isDragging ? 'dragging' : '' }`} > Drag me </div> ); } <Draggable /> <DropTarget /> import {useDrag} from 'react-aria'; function Draggable() { let { dragProps, isDragging } = useDrag({ getItems() { return [{ 'text/plain': 'hello world' }]; } }); return ( <div {...dragProps} role="button" tabIndex={0} className={`draggable ${ isDragging ? 'dragging' : '' }`} > Drag me </div> ); } <Draggable /> <DropTarget /> Drag me Drop here Show CSS .draggable { display: inline-block; vertical-align: top; border: 1px solid gray; padding: 10px; } .draggable.dragging { opacity: 0.5; } .droppable { width: 100px; height: 80px; border-radius: 6px; display: inline-block; padding: 20px; margin-left: 20px; border: 2px dotted gray; white-space: pre-wrap; } .droppable.target { border: 2px solid var(--blue); } .draggable { display: inline-block; vertical-align: top; border: 1px solid gray; padding: 10px; } .draggable.dragging { opacity: 0.5; } .droppable { width: 100px; height: 80px; border-radius: 6px; display: inline-block; padding: 20px; margin-left: 20px; border: 2px dotted gray; white-space: pre-wrap; } .droppable.target { border: 2px solid var(--blue); } .draggable { display: inline-block; vertical-align: top; border: 1px solid gray; padding: 10px; } .draggable.dragging { opacity: 0.5; } .droppable { width: 100px; height: 80px; border-radius: 6px; display: inline-block; padding: 20px; margin-left: 20px; border: 2px dotted gray; white-space: pre-wrap; } .droppable.target { border: 2px solid var(--blue); } ### DropTarget# The `DropTarget` component used above is defined below. See useDrop for more details and documentation. Show code import type {TextDropItem} from 'react-aria'; import {useDrop} from 'react-aria'; function DropTarget() { let [dropped, setDropped] = React.useState(null); let ref = React.useRef(null); let { dropProps, isDropTarget } = useDrop({ ref, async onDrop(e) { let items = await Promise.all( e.items .filter((item) => item.kind === 'text' && (item.types.has('text/plain') || item.types.has('my-app-custom-type')) ) .map(async (item: TextDropItem) => { if (item.types.has('my-app-custom-type')) { return JSON.parse(await item.getText('my-app-custom-type')); } else { return { message: await item.getText('text/plain') }; } }) ); setDropped(items); } }); let message = ['Drop here']; if (dropped) { message = dropped.map((d) => { let message = d.message; if (d.style === 'bold') { message = <strong>{message}</strong>; } else if (d.style === 'italic') { message = <em>{message}</em>; } return <div>{message}</div>; }); } return ( <div {...dropProps} role="button" tabIndex={0} ref={ref} className={`droppable ${isDropTarget ? 'target' : ''}`} > {message} </div> ); } import type {TextDropItem} from 'react-aria'; import {useDrop} from 'react-aria'; function DropTarget() { let [dropped, setDropped] = React.useState(null); let ref = React.useRef(null); let { dropProps, isDropTarget } = useDrop({ ref, async onDrop(e) { let items = await Promise.all( e.items .filter((item) => item.kind === 'text' && (item.types.has('text/plain') || item.types.has('my-app-custom-type')) ) .map(async (item: TextDropItem) => { if (item.types.has('my-app-custom-type')) { return JSON.parse( await item.getText('my-app-custom-type') ); } else { return { message: await item.getText('text/plain') }; } }) ); setDropped(items); } }); let message = ['Drop here']; if (dropped) { message = dropped.map((d) => { let message = d.message; if (d.style === 'bold') { message = <strong>{message}</strong>; } else if (d.style === 'italic') { message = <em>{message}</em>; } return <div>{message}</div>; }); } return ( <div {...dropProps} role="button" tabIndex={0} ref={ref} className={`droppable ${ isDropTarget ? 'target' : '' }`} > {message} </div> ); } import type {TextDropItem} from 'react-aria'; import {useDrop} from 'react-aria'; function DropTarget() { let [ dropped, setDropped ] = React.useState( null ); let ref = React.useRef( null ); let { dropProps, isDropTarget } = useDrop({ ref, async onDrop(e) { let items = await Promise .all( e.items .filter( (item) => item .kind === 'text' && (item .types .has( 'text/plain' ) || item .types .has( 'my-app-custom-type' )) ) .map( async ( item: TextDropItem ) => { if ( item .types .has( 'my-app-custom-type' ) ) { return JSON .parse( await item .getText( 'my-app-custom-type' ) ); } else { return { message: await item .getText( 'text/plain' ) }; } } ) ); setDropped(items); } }); let message = [ 'Drop here' ]; if (dropped) { message = dropped .map((d) => { let message = d.message; if ( d.style === 'bold' ) { message = ( <strong> {message} </strong> ); } else if ( d.style === 'italic' ) { message = ( <em> {message} </em> ); } return ( <div> {message} </div> ); }); } return ( <div {...dropProps} role="button" tabIndex={0} ref={ref} className={`droppable ${ isDropTarget ? 'target' : '' }`} > {message} </div> ); } ## Drag data# * * * Data for a draggable element can be provided in multiple formats at once. This allows drop targets to choose data in a format that they understand. For example, you could serialize a complex object as JSON in a custom format for use within your own application, and also provide plain text and/or rich HTML fallbacks that can be used when a user drops data in an external application (e.g. an email message). This can be done by returning multiple keys for an item from the `getItems` function. Types can either be a standard mime type for interoperability with external applications, or a custom string for use within your own app. In addition to providing items in multiple formats, you can also return multiple drag items from `getItems` to transfer multiple objects in a single drag operation. This example drags two items, each of which contains representations as plain text, HTML, and a custom app-specific data format. Dropping on the drop targets in this page will use the custom data format to render formatted items. If you drop in an external application supporting rich text, the HTML representation will be used. Dropping in a text editor will use the plain text format. function Draggable() { let {dragProps, isDragging} = useDrag({ getItems() { return [{ 'text/plain': 'hello world', 'text/html': '<strong>hello world</strong>', 'my-app-custom-type': JSON.stringify({ message: 'hello world', style: 'bold' }) }, { 'text/plain': 'foo bar', 'text/html': '<em>foo bar</em>', 'my-app-custom-type': JSON.stringify({ message: 'foo bar', style: 'italic' }) }]; } }); // ... } function Draggable() { let {dragProps, isDragging} = useDrag({ getItems() { return [{ 'text/plain': 'hello world', 'text/html': '<strong>hello world</strong>', 'my-app-custom-type': JSON.stringify({ message: 'hello world', style: 'bold' }) }, { 'text/plain': 'foo bar', 'text/html': '<em>foo bar</em>', 'my-app-custom-type': JSON.stringify({ message: 'foo bar', style: 'italic' }) }]; } }); // ... } function Draggable() { let { dragProps, isDragging } = useDrag({ getItems() { return [{ 'text/plain': 'hello world', 'text/html': '<strong>hello world</strong>', 'my-app-custom-type': JSON.stringify( { message: 'hello world', style: 'bold' } ) }, { 'text/plain': 'foo bar', 'text/html': '<em>foo bar</em>', 'my-app-custom-type': JSON.stringify( { message: 'foo bar', style: 'italic' } ) }]; } }); // ... } Drag me Drop here ## Drag previews# * * * By default, the drag preview shown under the user's pointer or finger is a copy of the original element that started the drag. A custom preview can be rendered using the `<DragPreview>` component. This accepts a function as a child which receives the dragged data that was returned by `getItems`, and returns a rendered preview for those items. The `DragPreview` is linked with `useDrag` via a ref, passed to the `preview` property. The `DragPreview` should be placed in the component hierarchy appropriately, so that it receives any React context or inherited styles that it needs to render correctly. This example renders a custom drag preview which shows the text of the first drag item. import {DragPreview} from 'react-aria'; function Draggable() { let preview = React.useRef(null); let { dragProps, isDragging } = useDrag({ preview, getItems() { return [{ 'text/plain': 'hello world' }]; } }); return ( <> <div {...dragProps} role="button" tabIndex={0} className={`draggable ${isDragging ? 'dragging' : ''}`} > Drag me </div> <DragPreview ref={preview}> {(items) => ( <div style={{ background: 'green', color: 'white' }}> {items[0]['text/plain']} </div> )} </DragPreview> </> ); } <Draggable /> <DropTarget /> import {DragPreview} from 'react-aria'; function Draggable() { let preview = React.useRef(null); let { dragProps, isDragging } = useDrag({ preview, getItems() { return [{ 'text/plain': 'hello world' }]; } }); return ( <> <div {...dragProps} role="button" tabIndex={0} className={`draggable ${ isDragging ? 'dragging' : '' }`} > Drag me </div> <DragPreview ref={preview}> {(items) => ( <div style={{ background: 'green', color: 'white' }} > {items[0]['text/plain']} </div> )} </DragPreview> </> ); } <Draggable /> <DropTarget /> import {DragPreview} from 'react-aria'; function Draggable() { let preview = React .useRef(null); let { dragProps, isDragging } = useDrag({ preview, getItems() { return [{ 'text/plain': 'hello world' }]; } }); return ( <> <div {...dragProps} role="button" tabIndex={0} className={`draggable ${ isDragging ? 'dragging' : '' }`} > Drag me </div> <DragPreview ref={preview} > {(items) => ( <div style={{ background: 'green', color: 'white' }} > {items[0][ 'text/plain' ]} </div> )} </DragPreview> </> ); } <Draggable /> <DropTarget /> Drag me Drop here ## Drop operations# * * * A `DropOperation` is an indication of what will happen when dragged data is dropped on a particular drop target. These are: * `move` – indicates that the dragged data will be moved from its source location to the target location. * `copy` – indicates that the dragged data will be copied to the target destination. * `link` – indicates that there will be a relationship established between the source and target locations. * `cancel` – indicates that the drag and drop operation will be canceled, resulting in no changes made to the source or target. Many operating systems display these in the form of a cursor change, e.g. a plus sign to indicate a copy operation. The user may also be able to use a modifier key to choose which drop operation to perform, such as Option or Alt to switch from move to copy. The `onDragEnd` event allows the drag source to respond when a drag that it initiated ends, either because it was dropped or because it was canceled by the user. The `dropOperation` property of the event object indicates the operation that was performed. For example, when data is moved, the UI could be updated to reflect this change by removing the original dragged element. This example removes the draggable element from the UI when a move operation is completed. Try holding the Option or Alt keys to change the operation to copy, and see how the behavior changes. function Draggable() { let [moved, setMoved] = React.useState(false); let {dragProps, isDragging} = useDrag({ getItems() { return [{ 'text/plain': 'hello world' }]; }, onDragEnd(e) { if (e.dropOperation === 'move') { setMoved(true); } } }); if (moved) { return null; } // ... } function Draggable() { let [moved, setMoved] = React.useState(false); let {dragProps, isDragging} = useDrag({ getItems() { return [{ 'text/plain': 'hello world' }]; }, onDragEnd(e) { if (e.dropOperation === 'move') { setMoved(true); } } }); if (moved) { return null; } // ... } function Draggable() { let [moved, setMoved] = React.useState( false ); let { dragProps, isDragging } = useDrag({ getItems() { return [{ 'text/plain': 'hello world' }]; }, onDragEnd(e) { if ( e.dropOperation === 'move' ) { setMoved(true); } } }); if (moved) { return null; } // ... } Drag me Drop here The drag source can also control which drop operations are allowed for the data. For example, if moving data is not allowed, and only copying is supported, the `getAllowedDropOperations` function could be implemented to indicate this. When you drag the element below, the cursor now shows the copy affordance by default, and pressing a modifier to switch drop operations results in the drop being canceled. function Draggable() { let {dragProps, isDragging} = useDrag({ getItems() { return [{ 'text/plain': 'hello world' }]; }, getAllowedDropOperations() { return ['copy']; } }); // ... } function Draggable() { let {dragProps, isDragging} = useDrag({ getItems() { return [{ 'text/plain': 'hello world' }]; }, getAllowedDropOperations() { return ['copy']; } }); // ... } function Draggable() { let { dragProps, isDragging } = useDrag({ getItems() { return [{ 'text/plain': 'hello world' }]; }, getAllowedDropOperations() { return ['copy']; } }); // ... } Drag me Drop here ## Drag button# * * * In cases where a draggable element has other interactions that conflict with accessible drag and drop (e.g. Enter key), or if the element is not focusable, an explicit drag affordance can be added. This acts as a button that keyboard and screen reader users can use to activate drag and drop. When the `hasDragButton` option is enabled, the keyboard interactions are moved from the returned `dragProps` to the `dragButtonProps` so that they can be applied to a separate element, while the mouse and touch dragging interactions remain in `dragProps`. import {useButton} from 'react-aria'; function Draggable() { let { dragProps, dragButtonProps, isDragging } = useDrag({ hasDragButton: true, getItems() { return [{ 'text/plain': 'hello world' }]; } }); let ref = React.useRef(null); let { buttonProps } = useButton( { ...dragButtonProps, elementType: 'div' }, ref ); return ( <div {...dragProps} className={`draggable ${isDragging ? 'dragging' : ''}`} style={{ display: 'inline-flex', alignItems: 'center', gap: 5 }} > <span {...buttonProps} aria-label="Drag" ref={ref} style={{ fontSize: 18 }} > ≡ </span> <span>Some text</span> <button onClick={() => alert('action')}>Action</button> </div> ); } <Draggable /> <DropTarget /> import {useButton} from 'react-aria'; function Draggable() { let { dragProps, dragButtonProps, isDragging } = useDrag({ hasDragButton: true, getItems() { return [{ 'text/plain': 'hello world' }]; } }); let ref = React.useRef(null); let { buttonProps } = useButton({ ...dragButtonProps, elementType: 'div' }, ref); return ( <div {...dragProps} className={`draggable ${ isDragging ? 'dragging' : '' }`} style={{ display: 'inline-flex', alignItems: 'center', gap: 5 }} > <span {...buttonProps} aria-label="Drag" ref={ref} style={{ fontSize: 18 }} > ≡ </span> <span>Some text</span> <button onClick={() => alert('action')}> Action </button> </div> ); } <Draggable /> <DropTarget /> import {useButton} from 'react-aria'; function Draggable() { let { dragProps, dragButtonProps, isDragging } = useDrag({ hasDragButton: true, getItems() { return [{ 'text/plain': 'hello world' }]; } }); let ref = React.useRef( null ); let { buttonProps } = useButton({ ...dragButtonProps, elementType: 'div' }, ref); return ( <div {...dragProps} className={`draggable ${ isDragging ? 'dragging' : '' }`} style={{ display: 'inline-flex', alignItems: 'center', gap: 5 }} > <span {...buttonProps} aria-label="Drag" ref={ref} style={{ fontSize: 18 }} > ≡ </span> <span> Some text </span> <button onClick={() => alert( 'action' )} > Action </button> </div> ); } <Draggable /> <DropTarget /> ≡Some textAction Drop here ## Disabling dragging# * * * If you need to temporarily disable dragging, you can pass the `isDisabled` option to `useDrag`. This will prevent dragging an element until it is re-enabled. import {useDrag} from 'react-aria'; function Draggable() { let { dragProps, isDragging } = useDrag({ getItems() { return [{ 'text/plain': 'hello world' }]; }, isDisabled: true }); return ( <div {...dragProps} role="button" tabIndex={0} className={`draggable ${isDragging ? 'dragging' : ''}`} > Drag me </div> ); } <Draggable /> import {useDrag} from 'react-aria'; function Draggable() { let { dragProps, isDragging } = useDrag({ getItems() { return [{ 'text/plain': 'hello world' }]; }, isDisabled: true }); return ( <div {...dragProps} role="button" tabIndex={0} className={`draggable ${ isDragging ? 'dragging' : '' }`} > Drag me </div> ); } <Draggable /> import {useDrag} from 'react-aria'; function Draggable() { let { dragProps, isDragging } = useDrag({ getItems() { return [{ 'text/plain': 'hello world' }]; }, isDisabled: true }); return ( <div {...dragProps} role="button" tabIndex={0} className={`draggable ${ isDragging ? 'dragging' : '' }`} > Drag me </div> ); } <Draggable /> Drag me --- ## Page: https://react-spectrum.adobe.com/react-aria/useDraggableCollection.html # useDraggableCollection Handles drag interactions for a collection component, with support for traditional mouse and touch based drag and drop, in addition to full parity for keyboard and screen reader users. <table><tbody><tr><th>install</th><td><code>yarn add react-aria</code></td></tr><tr><th>version</th><td>3.41.1</td></tr><tr><th>usage</th><td><code><span>import</span> {useDraggableCollection, useDraggableItem} <span>from</span> <span>'react-aria'</span></code></td></tr></tbody></table> View repository GitHub View package NPM ## API# * * * `useDraggableCollectionState( (props: DraggableCollectionStateOptions )): DraggableCollectionState ``useDraggableCollection( props: DraggableCollectionOptions , state: DraggableCollectionState , ref: RefObject <HTMLElement | | null> ): void` `useDraggableItem( (props: DraggableItemProps , , state: DraggableCollectionState )): DraggableItemResult ` ## Introduction# * * * Collection components built with hooks such as useListBox, useTable, and useGridList can support drag and drop interactions. Users can drag multiple selected items at once, or drag individual non-selected items. React Aria supports traditional mouse and touch based drag and drop, but also implements keyboard and screen reader friendly interactions. Users can press Enter on a draggable element to enter drag and drop mode. Then, they can press Tab to navigate between drop targets, and Enter to drop or Escape to cancel. Touch screen reader users can also drag by double tapping to activate drag and drop mode, swiping between drop targets, and double tapping again to drop. See the drag and drop introduction to learn more. ### Implementation# The `useDraggableCollection` hook implements drag interactions within any collection component, using state managed by` useDraggableCollectionState `. The` useDraggableItem `hook should be added to each individual item within the collection to make it draggable, combining props from the relevant hook (e.g. `useOption`). These hooks integrate with React Aria's selection system to enable dragging multiple selected items at once. ## Example# * * * This example renders a ListBox using the useListBox hook, and adds support for dragging items. The highlighted code sections below show the main additions for drag and drop compared with a normal listbox. import {Item, useDraggableCollectionState, useListState} from 'react-stately'; import {mergeProps, useDraggableCollection, useDraggableItem, useFocusRing, useListBox, useOption} from 'react-aria'; function ListBox(props) { // Setup listbox as normal. See the useListBox docs for more details. let state = useListState(props); let ref = React.useRef(null); let { listBoxProps } = useListBox( { ...props, // Prevent dragging from changing selection. shouldSelectOnPressUp: true }, state, ref ); // Setup drag state for the collection. let dragState = useDraggableCollectionState({ // Pass through events from props. ...props, // Collection and selection manager come from list state. collection: state.collection, selectionManager: state.selectionManager, // Provide data for each dragged item. This function could // also be provided by the user of the component. getItems: props.getItems || ((keys) => { return [...keys].map((key) => { let item = state.collection.getItem(key); return { 'text/plain': item.textValue }; }); }) }); useDraggableCollection(props, dragState, ref); return ( <ul {...listBoxProps} ref={ref}> {[...state.collection].map((item) => ( <Option key={item.key} item={item} state={state} dragState={dragState} /> ))} </ul> ); } function Option({ item, state, dragState }) { // Setup listbox option as normal. See useListBox docs for details. let ref = React.useRef(null); let { optionProps } = useOption({ key: item.key }, state, ref); let { isFocusVisible, focusProps } = useFocusRing(); // Register the item as a drag source. let { dragProps } = useDraggableItem({ key: item.key }, dragState); // Merge option props and dnd props, and render the item. return ( <li {...mergeProps(optionProps, dragProps, focusProps)} ref={ref} className={`option ${isFocusVisible ? 'focus-visible' : ''}`} > {item.rendered} </li> ); } <ListBox aria-label="Categories" selectionMode="multiple"> <Item>Animals</Item> <Item>People</Item> <Item>Plants</Item> </ListBox> <DropTarget /> import { Item, useDraggableCollectionState, useListState } from 'react-stately'; import { mergeProps, useDraggableCollection, useDraggableItem, useFocusRing, useListBox, useOption } from 'react-aria'; function ListBox(props) { // Setup listbox as normal. See the useListBox docs for more details. let state = useListState(props); let ref = React.useRef(null); let { listBoxProps } = useListBox( { ...props, // Prevent dragging from changing selection. shouldSelectOnPressUp: true }, state, ref ); // Setup drag state for the collection. let dragState = useDraggableCollectionState({ // Pass through events from props. ...props, // Collection and selection manager come from list state. collection: state.collection, selectionManager: state.selectionManager, // Provide data for each dragged item. This function could // also be provided by the user of the component. getItems: props.getItems || ((keys) => { return [...keys].map((key) => { let item = state.collection.getItem(key); return { 'text/plain': item.textValue }; }); }) }); useDraggableCollection(props, dragState, ref); return ( <ul {...listBoxProps} ref={ref}> {[...state.collection].map((item) => ( <Option key={item.key} item={item} state={state} dragState={dragState} /> ))} </ul> ); } function Option({ item, state, dragState }) { // Setup listbox option as normal. See useListBox docs for details. let ref = React.useRef(null); let { optionProps } = useOption( { key: item.key }, state, ref ); let { isFocusVisible, focusProps } = useFocusRing(); // Register the item as a drag source. let { dragProps } = useDraggableItem({ key: item.key }, dragState); // Merge option props and dnd props, and render the item. return ( <li {...mergeProps(optionProps, dragProps, focusProps)} ref={ref} className={`option ${ isFocusVisible ? 'focus-visible' : '' }`} > {item.rendered} </li> ); } <ListBox aria-label="Categories" selectionMode="multiple"> <Item>Animals</Item> <Item>People</Item> <Item>Plants</Item> </ListBox> <DropTarget /> import { Item, useDraggableCollectionState, useListState } from 'react-stately'; import { mergeProps, useDraggableCollection, useDraggableItem, useFocusRing, useListBox, useOption } from 'react-aria'; function ListBox(props) { // Setup listbox as normal. See the useListBox docs for more details. let state = useListState(props); let ref = React.useRef( null ); let { listBoxProps } = useListBox( { ...props, // Prevent dragging from changing selection. shouldSelectOnPressUp: true }, state, ref ); // Setup drag state for the collection. let dragState = useDraggableCollectionState( { // Pass through events from props. ...props, // Collection and selection manager come from list state. collection: state .collection, selectionManager: state .selectionManager, // Provide data for each dragged item. This function could // also be provided by the user of the component. getItems: props .getItems || ((keys) => { return [ ...keys ].map( (key) => { let item = state .collection .getItem( key ); return { 'text/plain': item .textValue }; } ); }) } ); useDraggableCollection( props, dragState, ref ); return ( <ul {...listBoxProps} ref={ref} > {[ ...state .collection ].map((item) => ( <Option key={item.key} item={item} state={state} dragState={dragState} /> ))} </ul> ); } function Option( { item, state, dragState } ) { // Setup listbox option as normal. See useListBox docs for details. let ref = React.useRef( null ); let { optionProps } = useOption( { key: item.key }, state, ref ); let { isFocusVisible, focusProps } = useFocusRing(); // Register the item as a drag source. let { dragProps } = useDraggableItem({ key: item.key }, dragState); // Merge option props and dnd props, and render the item. return ( <li {...mergeProps( optionProps, dragProps, focusProps )} ref={ref} className={`option ${ isFocusVisible ? 'focus-visible' : '' }`} > {item.rendered} </li> ); } <ListBox aria-label="Categories" selectionMode="multiple" > <Item>Animals</Item> <Item>People</Item> <Item>Plants</Item> </ListBox> <DropTarget /> * Animals * People * Plants Drop here Show CSS [role=listbox] { padding: 0; margin: 5px 0; list-style: none; box-shadow: inset 0 0 0 1px gray; max-width: 250px; outline: none; min-height: 50px; overflow: auto; } .option { padding: 3px 6px; outline: none; } .option[aria-selected=true] { background: blueviolet; color: white; } .option.focus-visible { box-shadow: inset 0 0 0 2px orange; } .option.drop-target { border-color: transparent; box-shadow: inset 0 0 0 2px var(--blue); } [role=listbox] { padding: 0; margin: 5px 0; list-style: none; box-shadow: inset 0 0 0 1px gray; max-width: 250px; outline: none; min-height: 50px; overflow: auto; } .option { padding: 3px 6px; outline: none; } .option[aria-selected=true] { background: blueviolet; color: white; } .option.focus-visible { box-shadow: inset 0 0 0 2px orange; } .option.drop-target { border-color: transparent; box-shadow: inset 0 0 0 2px var(--blue); } [role=listbox] { padding: 0; margin: 5px 0; list-style: none; box-shadow: inset 0 0 0 1px gray; max-width: 250px; outline: none; min-height: 50px; overflow: auto; } .option { padding: 3px 6px; outline: none; } .option[aria-selected=true] { background: blueviolet; color: white; } .option.focus-visible { box-shadow: inset 0 0 0 2px orange; } .option.drop-target { border-color: transparent; box-shadow: inset 0 0 0 2px var(--blue); } ### DropTarget# The `DropTarget` component used above is defined below. See useDrop for more details and documentation. Show code import type {TextDropItem} from 'react-aria'; import {useButton, useDrop} from 'react-aria'; function DropTarget() { let [dropped, setDropped] = React.useState(null); let ref = React.useRef(null); let { dropProps, isDropTarget } = useDrop({ ref, async onDrop(e) { let items = await Promise.all( e.items .filter((item) => item.kind === 'text' && (item.types.has('text/plain') || item.types.has('my-app-custom-type')) ) .map(async (item: TextDropItem) => { if (item.types.has('my-app-custom-type')) { return JSON.parse(await item.getText('my-app-custom-type')); } else { return { name: await item.getText('text/plain'), style: 'span' }; } }) ); setDropped(items); } }); let { buttonProps } = useButton({ elementType: 'div' }, ref); let message = ['Drop here']; if (dropped) { message = dropped.map((item, i) => ( <div key={i}> <item.style>{item.name}</item.style> </div> )); } return ( <div {...mergeProps(dropProps, buttonProps)} ref={ref} className={`droppable ${isDropTarget ? 'target' : ''}`} > {message} </div> ); } import type {TextDropItem} from 'react-aria'; import {useButton, useDrop} from 'react-aria'; function DropTarget() { let [dropped, setDropped] = React.useState(null); let ref = React.useRef(null); let { dropProps, isDropTarget } = useDrop({ ref, async onDrop(e) { let items = await Promise.all( e.items .filter((item) => item.kind === 'text' && (item.types.has('text/plain') || item.types.has('my-app-custom-type')) ) .map(async (item: TextDropItem) => { if (item.types.has('my-app-custom-type')) { return JSON.parse( await item.getText('my-app-custom-type') ); } else { return { name: await item.getText('text/plain'), style: 'span' }; } }) ); setDropped(items); } }); let { buttonProps } = useButton( { elementType: 'div' }, ref ); let message = ['Drop here']; if (dropped) { message = dropped.map((item, i) => ( <div key={i}> <item.style>{item.name}</item.style> </div> )); } return ( <div {...mergeProps(dropProps, buttonProps)} ref={ref} className={`droppable ${ isDropTarget ? 'target' : '' }`} > {message} </div> ); } import type {TextDropItem} from 'react-aria'; import { useButton, useDrop } from 'react-aria'; function DropTarget() { let [ dropped, setDropped ] = React.useState( null ); let ref = React.useRef( null ); let { dropProps, isDropTarget } = useDrop({ ref, async onDrop(e) { let items = await Promise .all( e.items .filter( (item) => item .kind === 'text' && (item .types .has( 'text/plain' ) || item .types .has( 'my-app-custom-type' )) ) .map( async ( item: TextDropItem ) => { if ( item .types .has( 'my-app-custom-type' ) ) { return JSON .parse( await item .getText( 'my-app-custom-type' ) ); } else { return { name: await item .getText( 'text/plain' ), style: 'span' }; } } ) ); setDropped(items); } }); let { buttonProps } = useButton({ elementType: 'div' }, ref); let message = [ 'Drop here' ]; if (dropped) { message = dropped .map((item, i) => ( <div key={i}> <item.style> {item.name} </item.style> </div> )); } return ( <div {...mergeProps( dropProps, buttonProps )} ref={ref} className={`droppable ${ isDropTarget ? 'target' : '' }`} > {message} </div> ); } Show CSS .droppable { width: 100px; height: 50px; border-radius: 6px; display: inline-block; padding: 20px; border: 2px dotted gray; white-space: pre-wrap; } .droppable.target { border: 2px solid var(--blue); } .droppable { width: 100px; height: 50px; border-radius: 6px; display: inline-block; padding: 20px; border: 2px dotted gray; white-space: pre-wrap; } .droppable.target { border: 2px solid var(--blue); } .droppable { width: 100px; height: 50px; border-radius: 6px; display: inline-block; padding: 20px; border: 2px dotted gray; white-space: pre-wrap; } .droppable.target { border: 2px solid var(--blue); } ## Drag data# * * * Data for a draggable element can be provided in multiple formats at once. This allows drop targets to choose data in a format that they understand. For example, you could serialize a complex object as JSON in a custom format for use within your own application, and also provide plain text and/or rich HTML fallbacks that can be used when a user drops data in an external application (e.g. an email message). This can be done by returning multiple keys for an item from the `getItems` function. Types can either be a standard mime type for interoperability with external applications, or a custom string for use within your own app. This example provides representations of each item as plain text, HTML, and a custom app-specific data format. Dropping on the drop targets in this page will use the custom data format to render formatted items. If you drop in an external application supporting rich text, the HTML representation will be used. Dropping in a text editor will use the plain text format. let items = new Map([ ['ps', { name: 'Photoshop', style: 'strong' }], ['xd', { name: 'XD', style: 'strong' }], ['id', { name: 'InDesign', style: 'strong' }], ['dw', { name: 'Dreamweaver', style: 'em' }], ['co', { name: 'Connect', style: 'em' }] ]); let getItems = (keys) => ( [...keys].map((key) => { let item = items.get(key); return { 'text/plain': item.name, 'text/html': `<${item.style}>${item.name}</${item.style}>`, 'my-app-custom-type': JSON.stringify(item) }; }) ); <ListBox aria-label="Adobe Apps" items={items} getItems={getItems} selectionMode="multiple" > {([id, item]) => ( <Item key={id} textValue={item.name}> <item.style>{item.name}</item.style> </Item> )} </ListBox> <DropTarget /> let items = new Map([ ['ps', { name: 'Photoshop', style: 'strong' }], ['xd', { name: 'XD', style: 'strong' }], ['id', { name: 'InDesign', style: 'strong' }], ['dw', { name: 'Dreamweaver', style: 'em' }], ['co', { name: 'Connect', style: 'em' }] ]); let getItems = (keys) => ( [...keys].map((key) => { let item = items.get(key); return { 'text/plain': item.name, 'text/html': `<${item.style}>${item.name}</${item.style}>`, 'my-app-custom-type': JSON.stringify(item) }; }) ); <ListBox aria-label="Adobe Apps" items={items} getItems={getItems} selectionMode="multiple" > {([id, item]) => ( <Item key={id} textValue={item.name}> <item.style>{item.name}</item.style> </Item> )} </ListBox> <DropTarget /> let items = new Map([ ['ps', { name: 'Photoshop', style: 'strong' }], ['xd', { name: 'XD', style: 'strong' }], ['id', { name: 'InDesign', style: 'strong' }], ['dw', { name: 'Dreamweaver', style: 'em' }], ['co', { name: 'Connect', style: 'em' }] ]); let getItems = ( keys ) => ( [...keys].map( (key) => { let item = items .get(key); return { 'text/plain': item.name, 'text/html': `<${item.style}>${item.name}</${item.style}>`, 'my-app-custom-type': JSON.stringify( item ) }; } ) ); <ListBox aria-label="Adobe Apps" items={items} getItems={getItems} selectionMode="multiple" > {([id, item]) => ( <Item key={id} textValue={item .name} > <item.style> {item.name} </item.style> </Item> )} </ListBox> <DropTarget /> * **Photoshop** * **XD** * **InDesign** * _Dreamweaver_ * _Connect_ Drop here ## Drag previews# * * * By default, the drag preview shown under the user's pointer or finger is a copy of the original element that started the drag. A custom preview can be rendered using the `<DragPreview>` component. This accepts a function as a child which receives the dragged data that was returned by `getItems`, and returns a rendered preview for those items. The `DragPreview` is linked with `useDraggableCollectionState` via a ref, passed to the `preview` property. The `DragPreview` should be placed in the component hierarchy appropriately, so that it receives any React context or inherited styles that it needs to render correctly. This example renders a custom drag preview which shows the number of items being dragged, or the contents if there is only one. import {DragPreview} from 'react-aria'; function ListBox(props) { // ... let preview = React.useRef(null); let dragState = useDraggableCollectionState({ collection: state.collection, selectionManager: state.selectionManager, preview, getItems(keys) { return [...keys].map((key) => { let item = state.collection.getItem(key); return { 'text/plain': item.textValue }; }); } }); useDraggableCollection(props, dragState, ref); return ( <ul {...listBoxProps} ref={ref}> {[...state.collection].map((item) => ( <Option key={item.key} item={item} state={state} dragState={dragState} /> ))} <DragPreview ref={preview}> {(items) => ( <div style={{ background: 'green', color: 'white' }}> {items.length > 1 ? `${items.length} items` : items[0]['text/plain']} </div> )} </DragPreview> </ul> ); } <ListBox aria-label="Categories" selectionMode="multiple"> <Item>Animals</Item> <Item>People</Item> <Item>Plants</Item> </ListBox> <DropTarget /> import {DragPreview} from 'react-aria'; function ListBox(props) { // ... let preview = React.useRef(null); let dragState = useDraggableCollectionState({ collection: state.collection, selectionManager: state.selectionManager, preview, getItems(keys) { return [...keys].map((key) => { let item = state.collection.getItem(key); return { 'text/plain': item.textValue }; }); } }); useDraggableCollection(props, dragState, ref); return ( <ul {...listBoxProps} ref={ref}> {[...state.collection].map((item) => ( <Option key={item.key} item={item} state={state} dragState={dragState} /> ))} <DragPreview ref={preview}> {(items) => ( <div style={{ background: 'green', color: 'white' }} > {items.length > 1 ? `${items.length} items` : items[0]['text/plain']} </div> )} </DragPreview> </ul> ); } <ListBox aria-label="Categories" selectionMode="multiple"> <Item>Animals</Item> <Item>People</Item> <Item>Plants</Item> </ListBox> <DropTarget /> import {DragPreview} from 'react-aria'; function ListBox(props) { // ... let preview = React .useRef(null); let dragState = useDraggableCollectionState( { collection: state .collection, selectionManager: state .selectionManager, preview, getItems(keys) { return [ ...keys ].map( (key) => { let item = state .collection .getItem( key ); return { 'text/plain': item .textValue }; } ); } } ); useDraggableCollection( props, dragState, ref ); return ( <ul {...listBoxProps} ref={ref} > {[ ...state .collection ].map((item) => ( <Option key={item.key} item={item} state={state} dragState={dragState} /> ))} <DragPreview ref={preview} > {(items) => ( <div style={{ background: 'green', color: 'white' }} > {items .length > 1 ? `${items.length} items` : items[0][ 'text/plain' ]} </div> )} </DragPreview> </ul> ); } <ListBox aria-label="Categories" selectionMode="multiple" > <Item>Animals</Item> <Item>People</Item> <Item>Plants</Item> </ListBox> <DropTarget /> * Animals * People * Plants Drop here ## Drop operations# * * * A `DropOperation` is an indication of what will happen when dragged data is dropped on a particular drop target. These are: * `move` – indicates that the dragged data will be moved from its source location to the target location. * `copy` – indicates that the dragged data will be copied to the target destination. * `link` – indicates that there will be a relationship established between the source and target locations. * `cancel` – indicates that the drag and drop operation will be canceled, resulting in no changes made to the source or target. Many operating systems display these in the form of a cursor change, e.g. a plus sign to indicate a copy operation. The user may also be able to use a modifier key to choose which drop operation to perform, such as Option or Alt to switch from move to copy. The `onDragEnd` event allows the drag source to respond when a drag that it initiated ends, either because it was dropped or because it was canceled by the user. The `dropOperation` property of the event object indicates the operation that was performed. For example, when data is moved, the UI could be updated to reflect this change by removing the original dragged items. This example removes the dragged items from the UI when a move operation is completed. It uses the useListData hook to help manage and update the list of items. Try holding the Option or Alt keys to change the operation to copy, and see how the behavior changes. import {useListData} from 'react-stately'; function Example() { let list = useListData({ initialItems: [ { id: 'a', textValue: 'Photoshop' }, { id: 'b', textValue: 'XD' }, { id: 'c', textValue: 'Dreamweaver' }, { id: 'd', textValue: 'InDesign' }, { id: 'e', textValue: 'Connect' } ] }); let onDragEnd = (e) => { if (e.dropOperation === 'move') { list.remove(...e.keys); } }; return ( <> <ListBox aria-label="Adobe apps" items={list.items} onDragEnd={onDragEnd} selectionMode="multiple" > {(item) => <Item>{item.textValue}</Item>} </ListBox> <DropTarget /> </> ); } import {useListData} from 'react-stately'; function Example() { let list = useListData({ initialItems: [ { id: 'a', textValue: 'Photoshop' }, { id: 'b', textValue: 'XD' }, { id: 'c', textValue: 'Dreamweaver' }, { id: 'd', textValue: 'InDesign' }, { id: 'e', textValue: 'Connect' } ] }); let onDragEnd = (e) => { if (e.dropOperation === 'move') { list.remove(...e.keys); } }; return ( <> <ListBox aria-label="Adobe apps" items={list.items} onDragEnd={onDragEnd} selectionMode="multiple" > {(item) => <Item>{item.textValue}</Item>} </ListBox> <DropTarget /> </> ); } import {useListData} from 'react-stately'; function Example() { let list = useListData( { initialItems: [ { id: 'a', textValue: 'Photoshop' }, { id: 'b', textValue: 'XD' }, { id: 'c', textValue: 'Dreamweaver' }, { id: 'd', textValue: 'InDesign' }, { id: 'e', textValue: 'Connect' } ] } ); let onDragEnd = (e) => { if ( e.dropOperation === 'move' ) { list.remove( ...e.keys ); } }; return ( <> <ListBox aria-label="Adobe apps" items={list .items} onDragEnd={onDragEnd} selectionMode="multiple" > {(item) => ( <Item> {item .textValue} </Item> )} </ListBox> <DropTarget /> </> ); } * Photoshop * XD * Dreamweaver * InDesign * Connect Drop here The drag source can also control which drop operations are allowed for the data. For example, if moving data is not allowed, and only copying is supported, the `getAllowedDropOperations` function could be implemented to indicate this. When you drag the element below, the cursor now shows the copy affordance by default, and pressing a modifier to switch drop operations results in the drop being canceled. <ListBox aria-label="Categories" getAllowedDropOperations={() => ['copy']} selectionMode="multiple" > <Item>Animals</Item> <Item>People</Item> <Item>Plants</Item> </ListBox> <DropTarget /> <ListBox aria-label="Categories" getAllowedDropOperations={() => ['copy']} selectionMode="multiple" > <Item>Animals</Item> <Item>People</Item> <Item>Plants</Item> </ListBox> <DropTarget /> <ListBox aria-label="Categories" getAllowedDropOperations={() => [ 'copy' ]} selectionMode="multiple" > <Item>Animals</Item> <Item>People</Item> <Item>Plants</Item> </ListBox> <DropTarget /> * Animals * People * Plants Drop here ## Reordering# * * * Drag and drop can be combined in the same collection component to allow reordering items. See useDroppableCollection for more details. import {ListDropTargetDelegate, ListKeyboardDelegate, useDropIndicator, useDroppableCollection} from 'react-aria'; import {useDroppableCollectionState} from 'react-stately'; function ReorderableListBox(props) { // ... // Setup react-stately and react-aria hooks for dropping. let dropState = useDroppableCollectionState({ ...props, collection: state.collection, selectionManager: state.selectionManager }); let { collectionProps } = useDroppableCollection( { ...props, // Provide drop targets for keyboard and pointer-based drag and drop. keyboardDelegate: new ListKeyboardDelegate( state.collection, state.disabledKeys, ref ), dropTargetDelegate: new ListDropTargetDelegate(state.collection, ref) }, dropState, ref ); return ( <ul {...mergeProps(listBoxProps, collectionProps)} ref={ref} > {[...state.collection].map((item) => ( <ReorderableOption key={item.key} item={item} state={state} dragState={dragState} dropState={dropState} /> ))} </ul> ); } function ReorderableOption({ item, state, dragState, dropState }) { // ... return ( <> <DropIndicator target={{ type: 'item', key: item.key, dropPosition: 'before' }} dropState={dropState} /> <li {...mergeProps(optionProps, dragProps, focusProps)} ref={ref} className={`option ${isFocusVisible ? 'focus-visible' : ''}`} > {item.rendered} </li> {state.collection.getKeyAfter(item.key) == null && ( <DropIndicator target={{ type: 'item', key: item.key, dropPosition: 'after' }} dropState={dropState} /> )} </> ); } function DropIndicator(props) { let ref = React.useRef(null); let { dropIndicatorProps, isHidden, isDropTarget } = useDropIndicator( props, props.dropState, ref ); if (isHidden) { return null; } return ( <li {...dropIndicatorProps} role="option" ref={ref} className={`drop-indicator ${isDropTarget ? 'drop-target' : ''}`} /> ); } import { ListDropTargetDelegate, ListKeyboardDelegate, useDropIndicator, useDroppableCollection } from 'react-aria'; import {useDroppableCollectionState} from 'react-stately'; function ReorderableListBox(props) { // ... // Setup react-stately and react-aria hooks for dropping. let dropState = useDroppableCollectionState({ ...props, collection: state.collection, selectionManager: state.selectionManager }); let { collectionProps } = useDroppableCollection( { ...props, // Provide drop targets for keyboard and pointer-based drag and drop. keyboardDelegate: new ListKeyboardDelegate( state.collection, state.disabledKeys, ref ), dropTargetDelegate: new ListDropTargetDelegate( state.collection, ref ) }, dropState, ref ); return ( <ul {...mergeProps(listBoxProps, collectionProps)} ref={ref} > {[...state.collection].map((item) => ( <ReorderableOption key={item.key} item={item} state={state} dragState={dragState} dropState={dropState} /> ))} </ul> ); } function ReorderableOption( { item, state, dragState, dropState } ) { // ... return ( <> <DropIndicator target={{ type: 'item', key: item.key, dropPosition: 'before' }} dropState={dropState} /> <li {...mergeProps(optionProps, dragProps, focusProps)} ref={ref} className={`option ${ isFocusVisible ? 'focus-visible' : '' }`} > {item.rendered} </li> {state.collection.getKeyAfter(item.key) == null && ( <DropIndicator target={{ type: 'item', key: item.key, dropPosition: 'after' }} dropState={dropState} /> )} </> ); } function DropIndicator(props) { let ref = React.useRef(null); let { dropIndicatorProps, isHidden, isDropTarget } = useDropIndicator(props, props.dropState, ref); if (isHidden) { return null; } return ( <li {...dropIndicatorProps} role="option" ref={ref} className={`drop-indicator ${ isDropTarget ? 'drop-target' : '' }`} /> ); } import { ListDropTargetDelegate, ListKeyboardDelegate, useDropIndicator, useDroppableCollection } from 'react-aria'; import {useDroppableCollectionState} from 'react-stately'; function ReorderableListBox( props ) { // ... // Setup react-stately and react-aria hooks for dropping. let dropState = useDroppableCollectionState( { ...props, collection: state .collection, selectionManager: state .selectionManager } ); let { collectionProps } = useDroppableCollection( { ...props, // Provide drop targets for keyboard and pointer-based drag and drop. keyboardDelegate: new ListKeyboardDelegate( state .collection, state .disabledKeys, ref ), dropTargetDelegate: new ListDropTargetDelegate( state .collection, ref ) }, dropState, ref ); return ( <ul {...mergeProps( listBoxProps, collectionProps )} ref={ref} > {[ ...state .collection ].map((item) => ( <ReorderableOption key={item.key} item={item} state={state} dragState={dragState} dropState={dropState} /> ))} </ul> ); } function ReorderableOption( { item, state, dragState, dropState } ) { // ... return ( <> <DropIndicator target={{ type: 'item', key: item.key, dropPosition: 'before' }} dropState={dropState} /> <li {...mergeProps( optionProps, dragProps, focusProps )} ref={ref} className={`option ${ isFocusVisible ? 'focus-visible' : '' }`} > {item.rendered} </li> {state.collection .getKeyAfter( item.key ) == null && ( <DropIndicator target={{ type: 'item', key: item.key, dropPosition: 'after' }} dropState={dropState} /> )} </> ); } function DropIndicator( props ) { let ref = React.useRef( null ); let { dropIndicatorProps, isHidden, isDropTarget } = useDropIndicator( props, props.dropState, ref ); if (isHidden) { return null; } return ( <li {...dropIndicatorProps} role="option" ref={ref} className={`drop-indicator ${ isDropTarget ? 'drop-target' : '' }`} /> ); } Now, we can render an example ListBox, which allows the user to reorder items. The `onReorder` event is triggered when the user drops dragged items which originated within the same collection. As above, useListData is used to manage the list items in this example, but it is not a requirement. import {useListData} from 'react-stately'; function Example() { let list = useListData({ initialItems: [ { id: 1, name: 'Cat' }, { id: 2, name: 'Dog' }, { id: 3, name: 'Kangaroo' }, { id: 4, name: 'Panda' }, { id: 5, name: 'Snake' } ] }); let onReorder = (e) => { if (e.target.dropPosition === 'before') { list.moveBefore(e.target.key, e.keys); } else if (e.target.dropPosition === 'after') { list.moveAfter(e.target.key, e.keys); } }; return ( <ReorderableListBox aria-label="Favorite animals" selectionMode="multiple" selectionBehavior="replace" items={list.items} onReorder={onReorder} > {(item) => <Item>{item.name}</Item>} </ReorderableListBox> ); } import {useListData} from 'react-stately'; function Example() { let list = useListData({ initialItems: [ { id: 1, name: 'Cat' }, { id: 2, name: 'Dog' }, { id: 3, name: 'Kangaroo' }, { id: 4, name: 'Panda' }, { id: 5, name: 'Snake' } ] }); let onReorder = (e) => { if (e.target.dropPosition === 'before') { list.moveBefore(e.target.key, e.keys); } else if (e.target.dropPosition === 'after') { list.moveAfter(e.target.key, e.keys); } }; return ( <ReorderableListBox aria-label="Favorite animals" selectionMode="multiple" selectionBehavior="replace" items={list.items} onReorder={onReorder} > {(item) => <Item>{item.name}</Item>} </ReorderableListBox> ); } import {useListData} from 'react-stately'; function Example() { let list = useListData( { initialItems: [ { id: 1, name: 'Cat' }, { id: 2, name: 'Dog' }, { id: 3, name: 'Kangaroo' }, { id: 4, name: 'Panda' }, { id: 5, name: 'Snake' } ] } ); let onReorder = (e) => { if ( e.target .dropPosition === 'before' ) { list.moveBefore( e.target.key, e.keys ); } else if ( e.target .dropPosition === 'after' ) { list.moveAfter( e.target.key, e.keys ); } }; return ( <ReorderableListBox aria-label="Favorite animals" selectionMode="multiple" selectionBehavior="replace" items={list.items} onReorder={onReorder} > {(item) => ( <Item> {item.name} </Item> )} </ReorderableListBox> ); } * Cat * Dog * Kangaroo * Panda * Snake Show CSS .drop-indicator { width: 100%; margin-left: 0; height: 2px; margin-bottom: -2px; outline: none; background: transparent; } .drop-indicator:last-child { margin-bottom: 0; margin-top: -2px; } .drop-indicator.drop-target { background: var(--blue); } .drop-indicator { width: 100%; margin-left: 0; height: 2px; margin-bottom: -2px; outline: none; background: transparent; } .drop-indicator:last-child { margin-bottom: 0; margin-top: -2px; } .drop-indicator.drop-target { background: var(--blue); } .drop-indicator { width: 100%; margin-left: 0; height: 2px; margin-bottom: -2px; outline: none; background: transparent; } .drop-indicator:last-child { margin-bottom: 0; margin-top: -2px; } .drop-indicator.drop-target { background: var(--blue); } ## Props# * * * The full list of props supported by draggable collections is available below. | Name | Type | Description | | --- | --- | --- | | `getItems` | `( (keys: Set<Key> )) => DragItem []` | A function that returns the items being dragged. | | `onDragStart` | `( (e: DraggableCollectionStartEvent )) => void` | Handler that is called when a drag operation is started. | | `onDragMove` | `( (e: DraggableCollectionMoveEvent )) => void` | Handler that is called when the drag is moved. | | `onDragEnd` | `( (e: DraggableCollectionEndEvent )) => void` | Handler that is called when the drag operation is ended, either as a result of a drop or a cancellation. | | `preview` | `RefObject< DragPreviewRenderer | null>` | The ref of the element that will be rendered as the drag preview while dragging. | | `getAllowedDropOperations` | `() => DropOperation []` | Function that returns the drop operations that are allowed for the dragged items. If not provided, all drop operations are allowed. | --- ## Page: https://react-spectrum.adobe.com/react-aria/useDrop.html # useDrop Handles drop interactions for an element, with support for traditional mouse and touch based drag and drop, in addition to full parity for keyboard and screen reader users. <table><tbody><tr><th>install</th><td><code>yarn add react-aria</code></td></tr><tr><th>version</th><td>3.41.1</td></tr><tr><th>usage</th><td><code><span>import</span> {useDrop} <span>from</span> <span>'react-aria'</span></code></td></tr></tbody></table> View repository GitHub View package NPM ## API# * * * `useDrop( (options: DropOptions )): DropResult ` ## Introduction# * * * Drag and drop is a common UI interaction that allows users to transfer data between two locations by directly moving a visual representation on screen. It is a flexible, efficient, and intuitive way for users to perform a variety of tasks, and is widely supported across both desktop and mobile operating systems. React Aria supports traditional mouse and touch based drag and drop, but also implements keyboard and screen reader friendly interactions. Users can press Enter on a draggable element to enter drag and drop mode. Then, they can press Tab to navigate between drop targets, and Enter to drop or Escape to cancel. Touch screen reader users can also drag by double tapping to activate drag and drop mode, swiping between drop targets, and double tapping again to drop. See the drag and drop introduction to learn more. ## Example# * * * This example shows how to make a simple drop target that accepts plain text data. In order to support keyboard and screen reader drag interactions, the element must be focusable and have an ARIA role (in this case, `button`). While a drag is hovered over it, a blue outline is rendered by applying an additional CSS class. import type {TextDropItem} from 'react-aria'; import {useDrop} from 'react-aria'; function DropTarget() { let [dropped, setDropped] = React.useState(null); let ref = React.useRef(null); let { dropProps, isDropTarget } = useDrop({ ref, async onDrop(e) { let items = await Promise.all( e.items .filter((item) => item.kind === 'text' && item.types.has('text/plain') ) .map((item: TextDropItem) => item.getText('text/plain')) ); setDropped(items.join('\n')); } }); return ( <div {...dropProps} role="button" tabIndex={0} ref={ref} className={`droppable ${isDropTarget ? 'target' : ''}`} > {dropped || 'Drop here'} </div> ); } <Draggable /> <DropTarget /> import type {TextDropItem} from 'react-aria'; import {useDrop} from 'react-aria'; function DropTarget() { let [dropped, setDropped] = React.useState(null); let ref = React.useRef(null); let { dropProps, isDropTarget } = useDrop({ ref, async onDrop(e) { let items = await Promise.all( e.items .filter((item) => item.kind === 'text' && item.types.has('text/plain') ) .map((item: TextDropItem) => item.getText('text/plain') ) ); setDropped(items.join('\n')); } }); return ( <div {...dropProps} role="button" tabIndex={0} ref={ref} className={`droppable ${ isDropTarget ? 'target' : '' }`} > {dropped || 'Drop here'} </div> ); } <Draggable /> <DropTarget /> import type {TextDropItem} from 'react-aria'; import {useDrop} from 'react-aria'; function DropTarget() { let [ dropped, setDropped ] = React.useState( null ); let ref = React.useRef( null ); let { dropProps, isDropTarget } = useDrop({ ref, async onDrop(e) { let items = await Promise .all( e.items .filter( (item) => item .kind === 'text' && item .types .has( 'text/plain' ) ) .map(( item: TextDropItem ) => item .getText( 'text/plain' ) ) ); setDropped( items.join('\n') ); } }); return ( <div {...dropProps} role="button" tabIndex={0} ref={ref} className={`droppable ${ isDropTarget ? 'target' : '' }`} > {dropped || 'Drop here'} </div> ); } <Draggable /> <DropTarget /> Drag me Drop here Show CSS .draggable { display: inline-block; vertical-align: top; border: 1px solid gray; padding: 10px; margin-right: 20px; } .draggable.dragging { opacity: 0.5; } .droppable { width: 100px; height: 80px; border-radius: 6px; display: inline-block; padding: 20px; border: 2px dotted gray; white-space: pre-wrap; overflow: auto; } .droppable.target { border: 2px solid var(--blue); } .draggable { display: inline-block; vertical-align: top; border: 1px solid gray; padding: 10px; margin-right: 20px; } .draggable.dragging { opacity: 0.5; } .droppable { width: 100px; height: 80px; border-radius: 6px; display: inline-block; padding: 20px; border: 2px dotted gray; white-space: pre-wrap; overflow: auto; } .droppable.target { border: 2px solid var(--blue); } .draggable { display: inline-block; vertical-align: top; border: 1px solid gray; padding: 10px; margin-right: 20px; } .draggable.dragging { opacity: 0.5; } .droppable { width: 100px; height: 80px; border-radius: 6px; display: inline-block; padding: 20px; border: 2px dotted gray; white-space: pre-wrap; overflow: auto; } .droppable.target { border: 2px solid var(--blue); } ### Draggable# The `Draggable` component used above is defined below. See useDrag for more details and documentation. Show code import {useDrag} from 'react-aria'; function Draggable() { let { dragProps, isDragging } = useDrag({ getItems() { return [{ 'text/plain': 'hello world', 'my-app-custom-type': JSON.stringify({ message: 'hello world' }) }]; } }); return ( <div {...dragProps} role="button" tabIndex={0} className={`draggable ${isDragging ? 'dragging' : ''}`} > Drag me </div> ); } import {useDrag} from 'react-aria'; function Draggable() { let { dragProps, isDragging } = useDrag({ getItems() { return [{ 'text/plain': 'hello world', 'my-app-custom-type': JSON.stringify({ message: 'hello world' }) }]; } }); return ( <div {...dragProps} role="button" tabIndex={0} className={`draggable ${ isDragging ? 'dragging' : '' }`} > Drag me </div> ); } import {useDrag} from 'react-aria'; function Draggable() { let { dragProps, isDragging } = useDrag({ getItems() { return [{ 'text/plain': 'hello world', 'my-app-custom-type': JSON.stringify( { message: 'hello world' } ) }]; } }); return ( <div {...dragProps} role="button" tabIndex={0} className={`draggable ${ isDragging ? 'dragging' : '' }`} > Drag me </div> ); } ## Drop data# * * * `useDrop` allows users to drop one or more **drag items**, each of which contains data to be transferred from the drag source to drop target. There are three kinds of drag items: * `text` – represents data inline as a string in one or more formats * `file` – references a file on the user's device * `directory` – references the contents of a directory ### Text# A `TextDropItem` represents textual data in one or more different formats. These may be either standard mime types or custom app-specific formats. Representing data in multiple formats allows drop targets both within and outside an application to choose data in a format that they understand. For example, a complex object may be serialized in a custom format for use within an application, with fallbacks in plain text and/or rich HTML that can be used when a user drops data from an external application. The example below finds the first available item that includes a custom app-specific type. The same draggable component as used in the above example is used here, but rather than displaying the plain text representation, the custom format is used instead. function DropTarget() { let [dropped, setDropped] = React.useState(null); let ref = React.useRef(null); let { dropProps, isDropTarget } = useDrop({ ref, async onDrop(e) { let item = e.items.find((item) => item.kind === 'text' && item.types.has('my-app-custom-type') ) as TextDropItem; if (item) { setDropped(await item.getText('my-app-custom-type')); } } }); // ... } function DropTarget() { let [dropped, setDropped] = React.useState(null); let ref = React.useRef(null); let { dropProps, isDropTarget } = useDrop({ ref, async onDrop(e) { let item = e.items.find((item) => item.kind === 'text' && item.types.has('my-app-custom-type') ) as TextDropItem; if (item) { setDropped( await item.getText('my-app-custom-type') ); } } }); // ... } function DropTarget() { let [ dropped, setDropped ] = React.useState( null ); let ref = React.useRef( null ); let { dropProps, isDropTarget } = useDrop({ ref, async onDrop(e) { let item = e.items .find((item) => item.kind === 'text' && item.types.has( 'my-app-custom-type' ) ) as TextDropItem; if (item) { setDropped( await item .getText( 'my-app-custom-type' ) ); } } }); // ... } Drag me Drop here ### Files# A `FileDropItem` references a file on the user's device. It includes the name and mime type of the file, and methods to read the contents as plain text, or retrieve a native File object which can be attached to form data for uploading. This example accepts JPEG and PNG image files, and renders them by creating a local object URL. import type {FileDropItem} from 'react-aria'; function DropTarget() { let [file, setFile] = React.useState(null); let ref = React.useRef(null); let { dropProps, isDropTarget } = useDrop({ ref, async onDrop(e) { let item = e.items.find((item) => item.kind === 'file' && (item.type === 'image/jpeg' || item.type === 'image/png') ) as FileDropItem; if (item) { setFile(URL.createObjectURL(await item.getFile())); } } }); return ( <div {...dropProps} role="button" tabIndex={0} ref={ref} className={`droppable ${isDropTarget ? 'target' : ''}`} > {file ? ( <img src={file} style={{ width: '100%', height: '100%', objectFit: 'contain' }} /> ) : 'Drop image here'} </div> ); } import type {FileDropItem} from 'react-aria'; function DropTarget() { let [file, setFile] = React.useState(null); let ref = React.useRef(null); let { dropProps, isDropTarget } = useDrop({ ref, async onDrop(e) { let item = e.items.find((item) => item.kind === 'file' && (item.type === 'image/jpeg' || item.type === 'image/png') ) as FileDropItem; if (item) { setFile(URL.createObjectURL(await item.getFile())); } } }); return ( <div {...dropProps} role="button" tabIndex={0} ref={ref} className={`droppable ${ isDropTarget ? 'target' : '' }`} > {file ? ( <img src={file} style={{ width: '100%', height: '100%', objectFit: 'contain' }} /> ) : 'Drop image here'} </div> ); } import type {FileDropItem} from 'react-aria'; function DropTarget() { let [file, setFile] = React.useState(null); let ref = React.useRef( null ); let { dropProps, isDropTarget } = useDrop({ ref, async onDrop(e) { let item = e.items .find((item) => item.kind === 'file' && (item.type === 'image/jpeg' || item.type === 'image/png') ) as FileDropItem; if (item) { setFile( URL .createObjectURL( await item .getFile() ) ); } } }); return ( <div {...dropProps} role="button" tabIndex={0} ref={ref} className={`droppable ${ isDropTarget ? 'target' : '' }`} > {file ? ( <img src={file} style={{ width: '100%', height: '100%', objectFit: 'contain' }} /> ) : 'Drop image here'} </div> ); } Drop image here ### Directories# A `DirectoryDropItem` references the contents of a directory on the user's device. It includes the name of the directory, as well as a method to iterate through the files and folders within the directory. The contents of any folders within the directory can be accessed recursively. The `getEntries` method returns an async iterable object, which can be used in a `for await...of` loop. This provides each item in the directory as either a `FileDropItem` or` DirectoryDropItem `, and you can access the contents of each file as discussed above. This example renders the file names within a dropped directory in a grid. import type {DirectoryDropItem} from 'react-aria'; import File from '@spectrum-icons/workflow/FileTxt'; import Folder from '@spectrum-icons/workflow/Folder'; function DropTarget() { let [files, setFiles] = React.useState(null); let ref = React.useRef(null); let { dropProps, isDropTarget } = useDrop({ ref, async onDrop(e) { // Find the first dropped item that is a directory. let dir = e.items.find((item) => item.kind === 'directory' ) as DirectoryDropItem; if (dir) { // Read entries in directory and update state with relevant info. let files = []; for await (let entry of dir.getEntries()) { files.push({ name: entry.name, kind: entry.kind }); } setFiles(files); } } }); let contents = <>Drop directory here</>; if (files) { contents = ( <ul> {files.map((f) => ( <li key={f.name}> {f.kind === 'directory' ? <Folder /> : <File />} <span>{f.name}</span> </li> ))} </ul> ); } return ( <div {...dropProps} role="button" tabIndex={0} ref={ref} className={`droppable grid ${isDropTarget ? 'target' : ''}`} > {contents} </div> ); } import type {DirectoryDropItem} from 'react-aria'; import File from '@spectrum-icons/workflow/FileTxt'; import Folder from '@spectrum-icons/workflow/Folder'; function DropTarget() { let [files, setFiles] = React.useState(null); let ref = React.useRef(null); let { dropProps, isDropTarget } = useDrop({ ref, async onDrop(e) { // Find the first dropped item that is a directory. let dir = e.items.find((item) => item.kind === 'directory' ) as DirectoryDropItem; if (dir) { // Read entries in directory and update state with relevant info. let files = []; for await (let entry of dir.getEntries()) { files.push({ name: entry.name, kind: entry.kind }); } setFiles(files); } } }); let contents = <>Drop directory here</>; if (files) { contents = ( <ul> {files.map((f) => ( <li key={f.name}> {f.kind === 'directory' ? <Folder /> : <File />} <span>{f.name}</span> </li> ))} </ul> ); } return ( <div {...dropProps} role="button" tabIndex={0} ref={ref} className={`droppable grid ${ isDropTarget ? 'target' : '' }`} > {contents} </div> ); } import type {DirectoryDropItem} from 'react-aria'; import File from '@spectrum-icons/workflow/FileTxt'; import Folder from '@spectrum-icons/workflow/Folder'; function DropTarget() { let [files, setFiles] = React.useState(null); let ref = React.useRef( null ); let { dropProps, isDropTarget } = useDrop({ ref, async onDrop(e) { // Find the first dropped item that is a directory. let dir = e.items .find((item) => item.kind === 'directory' ) as DirectoryDropItem; if (dir) { // Read entries in directory and update state with relevant info. let files = []; for await ( let entry of dir .getEntries() ) { files.push({ name: entry.name, kind: entry.kind }); } setFiles(files); } } }); let contents = ( <> Drop directory here </> ); if (files) { contents = ( <ul> {files.map( (f) => ( <li key={f .name} > {f.kind === 'directory' ? ( <Folder /> ) : ( <File /> )} <span> {f.name} </span> </li> ) )} </ul> ); } return ( <div {...dropProps} role="button" tabIndex={0} ref={ref} className={`droppable grid ${ isDropTarget ? 'target' : '' }`} > {contents} </div> ); } Drop directory here Show CSS .grid { display: block; width: auto; height: auto; min-height: 80px; } .grid ul { display: grid; grid-template-columns: repeat(auto-fit, 100px); list-style: none; margin: 0; padding: 0; gap: 20px; } .grid li { display: flex; align-items: center; gap: 8px; } .grid li svg { flex: 0 0 auto; } .grid li span { flex: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .grid { display: block; width: auto; height: auto; min-height: 80px; } .grid ul { display: grid; grid-template-columns: repeat(auto-fit, 100px); list-style: none; margin: 0; padding: 0; gap: 20px; } .grid li { display: flex; align-items: center; gap: 8px; } .grid li svg { flex: 0 0 auto; } .grid li span { flex: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .grid { display: block; width: auto; height: auto; min-height: 80px; } .grid ul { display: grid; grid-template-columns: repeat(auto-fit, 100px); list-style: none; margin: 0; padding: 0; gap: 20px; } .grid li { display: flex; align-items: center; gap: 8px; } .grid li svg { flex: 0 0 auto; } .grid li span { flex: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } ## Drop operations# * * * A `DropOperation` is an indication of what will happen when dragged data is dropped on a particular drop target. These are: * `move` – indicates that the dragged data will be moved from its source location to the target location. * `copy` – indicates that the dragged data will be copied to the target destination. * `link` – indicates that there will be a relationship established between the source and target locations. * `cancel` – indicates that the drag and drop operation will be canceled, resulting in no changes made to the source or target. Many operating systems display these in the form of a cursor change, e.g. a plus sign to indicate a copy operation. The user may also be able to use a modifier key to choose which drop operation to perform, such as Option or Alt to switch from move to copy. The drag source can specify which drop operations are allowed for the dragged data (see the useDrag docs for how to customize this). By default, the first allowed operation is allowed by drop targets, meaning that the drop target accepts data of any type and operation. ### getDropOperation# The `getDropOperation` function passed to `useDrop` can be used to provide appropriate feedback to the user when a drag hovers over the drop target. If a drop target only supports data of specific types (e.g. images, videos, text, etc.), then it should implement `getDropOperation` and return `'cancel'` for types that aren't supported. This will prevent visual feedback indicating that the drop target accepts the dragged data when this is not true. When the data is supported, either return one of the drop operations in `allowedOperation` or a specific drop operation if only that drop operation is supported. If the returned operation is not in `allowedOperations`, then the drop target will act as if `'cancel'` was returned. In the below example, the drop target only supports dropping PNG images. If a PNG is dragged over the target, it will be highlighted and the operating system displays a copy cursor. If another type is dragged over the target, then there is no visual feedback, indicating that a drop is not accepted there. If the user holds a modifier key such as Control while dragging over the drop target in order to change the drop operation, then the drop target does not accept the drop. function DropTarget() { let [file, setFile] = React.useState(null); let ref = React.useRef(null); let { dropProps, isDropTarget } = useDrop({ ref, getDropOperation(types, allowedOperations) { return types.has('image/png') ? 'copy' : 'cancel'; }, async onDrop(e) { let item = e.items.find((item) => item.kind === 'file' && item.type === 'image/png' ) as FileDropItem; if (item) { setFile(URL.createObjectURL(await item.getFile())); } } }); // ... } function DropTarget() { let [file, setFile] = React.useState(null); let ref = React.useRef(null); let { dropProps, isDropTarget } = useDrop({ ref, getDropOperation(types, allowedOperations) { return types.has('image/png') ? 'copy' : 'cancel'; }, async onDrop(e) { let item = e.items.find((item) => item.kind === 'file' && item.type === 'image/png' ) as FileDropItem; if (item) { setFile(URL.createObjectURL(await item.getFile())); } } }); // ... } function DropTarget() { let [file, setFile] = React.useState(null); let ref = React.useRef( null ); let { dropProps, isDropTarget } = useDrop({ ref, getDropOperation( types, allowedOperations ) { return types.has( 'image/png' ) ? 'copy' : 'cancel'; }, async onDrop(e) { let item = e.items .find((item) => item.kind === 'file' && item.type === 'image/png' ) as FileDropItem; if (item) { setFile( URL .createObjectURL( await item .getFile() ) ); } } }); // ... } Drop image here ### onDrop# The `onDrop` event also includes the `dropOperation`. This can be used to perform different actions accordingly, for example, when communicating with a backend API. function DropTarget(props) { let ref = React.useRef(null); let { dropProps, isDropTarget } = useDrop({ ref, async onDrop(e) { let item = e.items.find((item) => item.kind === 'text' && item.types.has('my-app-file') ) as TextDropItem; if (!item) { return; } let data = JSON.parse(await item.getText('my-app-file')); switch (e.dropOperation) { case 'move': MyAppFileService.move(data.filePath, props.filePath); break; case 'copy': MyAppFileService.copy(data.filePath, props.filePath); break; case 'link': MyAppFileService.link(data.filePath, props.filePath); break; } } }); // ... } function DropTarget(props) { let ref = React.useRef(null); let { dropProps, isDropTarget } = useDrop({ ref, async onDrop(e) { let item = e.items.find((item) => item.kind === 'text' && item.types.has('my-app-file') ) as TextDropItem; if (!item) { return; } let data = JSON.parse( await item.getText('my-app-file') ); switch (e.dropOperation) { case 'move': MyAppFileService.move( data.filePath, props.filePath ); break; case 'copy': MyAppFileService.copy( data.filePath, props.filePath ); break; case 'link': MyAppFileService.link( data.filePath, props.filePath ); break; } } }); // ... } function DropTarget( props ) { let ref = React.useRef( null ); let { dropProps, isDropTarget } = useDrop({ ref, async onDrop(e) { let item = e.items .find((item) => item.kind === 'text' && item.types.has( 'my-app-file' ) ) as TextDropItem; if (!item) { return; } let data = JSON .parse( await item .getText( 'my-app-file' ) ); switch ( e.dropOperation ) { case 'move': MyAppFileService .move( data .filePath, props .filePath ); break; case 'copy': MyAppFileService .copy( data .filePath, props .filePath ); break; case 'link': MyAppFileService .link( data .filePath, props .filePath ); break; } } }); // ... } ## Events# * * * Drop targets receive a number of events during a drag session. These are: | Name | Type | Description | | --- | --- | --- | | `onDropEnter` | `( (e: DropEnterEvent )) => void` | Handler that is called when a valid drag enters the drop target. | | `onDropMove` | `( (e: DropMoveEvent )) => void` | Handler that is called when a valid drag is moved within the drop target. | | `onDropActivate` | `( (e: DropActivateEvent )) => void` | Handler that is called after a valid drag is held over the drop target for a period of time. This typically opens the item so that the user can drop within it. | | `onDropExit` | `( (e: DropExitEvent )) => void` | Handler that is called when a valid drag exits the drop target. | | `onDrop` | `( (e: DropEvent )) => void` | Handler that is called when a valid drag is dropped on the drop target. | This example logs all events that occur within the drop target: function DropTarget() { let [events, setEvents] = React.useState([]); let onEvent = (e) => setEvents((events) => [JSON.stringify(e), ...events]); let ref = React.useRef(null); let { dropProps, isDropTarget } = useDrop({ ref, onDropEnter: onEvent, onDropMove: onEvent, onDropExit: onEvent, onDrop: onEvent }); return ( <ul {...dropProps} role="button" tabIndex={0} ref={ref} className={`droppable ${isDropTarget ? 'target' : ''}`} style={{ display: 'block', width: 'auto' }} > {events.map((e, i) => <li key={i}>{e}</li>)} </ul> ); } <Draggable /> <DropTarget /> function DropTarget() { let [events, setEvents] = React.useState([]); let onEvent = (e) => setEvents((events) => [JSON.stringify(e), ...events]); let ref = React.useRef(null); let { dropProps, isDropTarget } = useDrop({ ref, onDropEnter: onEvent, onDropMove: onEvent, onDropExit: onEvent, onDrop: onEvent }); return ( <ul {...dropProps} role="button" tabIndex={0} ref={ref} className={`droppable ${ isDropTarget ? 'target' : '' }`} style={{ display: 'block', width: 'auto' }} > {events.map((e, i) => <li key={i}>{e}</li>)} </ul> ); } <Draggable /> <DropTarget /> function DropTarget() { let [ events, setEvents ] = React.useState([]); let onEvent = (e) => setEvents( (events) => [ JSON.stringify( e ), ...events ] ); let ref = React.useRef( null ); let { dropProps, isDropTarget } = useDrop({ ref, onDropEnter: onEvent, onDropMove: onEvent, onDropExit: onEvent, onDrop: onEvent }); return ( <ul {...dropProps} role="button" tabIndex={0} ref={ref} className={`droppable ${ isDropTarget ? 'target' : '' }`} style={{ display: 'block', width: 'auto' }} > {events.map(( e, i ) => ( <li key={i}> {e} </li> ))} </ul> ); } <Draggable /> <DropTarget /> Drag me ## Disabling dropping# * * * If you need to temporarily disable dropping, you can pass the `isDisabled` option to `useDrop`. This will prevent the drop target from accepting any drops until it is re-enabled. import type {TextDropItem} from 'react-aria'; import {useDrop} from 'react-aria'; function DropTarget() { let [dropped, setDropped] = React.useState(null); let ref = React.useRef(null); let { dropProps, isDropTarget } = useDrop({ ref, async onDrop(e) { let items = await Promise.all( e.items .filter((item) => item.kind === 'text' && item.types.has('text/plain') ) .map((item: TextDropItem) => item.getText('text/plain')) ); setDropped(items.join('\n')); }, isDisabled: true }); return ( <div {...dropProps} role="button" tabIndex={0} ref={ref} className={`droppable ${isDropTarget ? 'target' : ''}`} > {dropped || 'Drop here'} </div> ); } <Draggable /> <DropTarget /> import type {TextDropItem} from 'react-aria'; import {useDrop} from 'react-aria'; function DropTarget() { let [dropped, setDropped] = React.useState(null); let ref = React.useRef(null); let { dropProps, isDropTarget } = useDrop({ ref, async onDrop(e) { let items = await Promise.all( e.items .filter((item) => item.kind === 'text' && item.types.has('text/plain') ) .map((item: TextDropItem) => item.getText('text/plain') ) ); setDropped(items.join('\n')); }, isDisabled: true }); return ( <div {...dropProps} role="button" tabIndex={0} ref={ref} className={`droppable ${ isDropTarget ? 'target' : '' }`} > {dropped || 'Drop here'} </div> ); } <Draggable /> <DropTarget /> import type {TextDropItem} from 'react-aria'; import {useDrop} from 'react-aria'; function DropTarget() { let [ dropped, setDropped ] = React.useState( null ); let ref = React.useRef( null ); let { dropProps, isDropTarget } = useDrop({ ref, async onDrop(e) { let items = await Promise .all( e.items .filter( (item) => item .kind === 'text' && item .types .has( 'text/plain' ) ) .map(( item: TextDropItem ) => item .getText( 'text/plain' ) ) ); setDropped( items.join('\n') ); }, isDisabled: true }); return ( <div {...dropProps} role="button" tabIndex={0} ref={ref} className={`droppable ${ isDropTarget ? 'target' : '' }`} > {dropped || 'Drop here'} </div> ); } <Draggable /> <DropTarget /> Drag me Drop here --- ## Page: https://react-spectrum.adobe.com/react-aria/useDroppableCollection.html # useDroppableCollection Handles drop interactions for a collection component, with support for traditional mouse and touch based drag and drop, in addition to full parity for keyboard and screen reader users. <table><tbody><tr><th>install</th><td><code>yarn add react-aria</code></td></tr><tr><th>version</th><td>3.41.1</td></tr><tr><th>usage</th><td><code><span>import</span> {useDroppableCollection, useDroppableItem, useDropIndicator} <span>from</span> <span>'react-aria'</span></code></td></tr></tbody></table> View repository GitHub View package NPM ## API# * * * `useDroppableCollectionState( (props: DroppableCollectionStateOptions )): DroppableCollectionState ``useDroppableCollection( props: DroppableCollectionOptions , state: DroppableCollectionState , ref: RefObject <HTMLElement | | null> ): DroppableCollectionResult ``useDroppableItem( options: DroppableItemOptions , state: DroppableCollectionState , ref: RefObject <HTMLElement | | null> ): DroppableItemResult ``useDropIndicator( props: DropIndicatorProps , state: DroppableCollectionState , ref: RefObject <HTMLElement | | null> ): DropIndicatorAria ` ## Introduction# * * * Collection components built with hooks such as useListBox, useTable, and useGridList can support drag and drop interactions. Users can drop data on the collection as a whole, on individual items, insert new items between existing ones, or reorder items. React Aria supports traditional mouse and touch based drag and drop, but also implements keyboard and screen reader friendly interactions. Users can press Enter on a draggable element to enter drag and drop mode. Then, they can press Tab to navigate between drop targets. A droppable collection is treated as a single drop target, so that users can easily tab past it to get to the next drop target. Within a droppable collection, keys such as ArrowDown and ArrowUp can be used to select a _drop position_, such as on an item, or between items. These are represented using `DropTarget` objects. The keyboard interactions used within a collection may differ depending on the type or layout. For example, in a grid the ArrowLeft and ArrowRight may also be used, whereas they may not within a list. In general, the keyboard interactions used during drag and drop match those used when navigating the collection normally. See the drag and drop introduction to learn more. ### Implementation# The `useDroppableCollection` hook implements drop interactions within any collection component, using state managed by` useDroppableCollectionState `. The props it returns should be combined with those from the collection component you're using, such as `useListBox`. The` useDroppableItem `hook should be added to each individual item within the collection, combining props from the relevant hook (e.g. `useOption`). To support dropping between items, the `useDropIndicator` hook can be used to add additional elements between each item, for example, rendering a line when a user drags between two items. These elements must be implemented according to the relevant ARIA pattern. For example, within a listbox, drop indicators must be implemented using `role="option"`, and within a grid, they must use `role="row"` and `role="gridcell"` to ensure the accessibility tree is valid. Interactions like keyboard navigation, and drop target positioning may differ depending on the component and layout of items. These are implemented using the `KeyboardDelegate` and` DropTargetDelegate `interfaces, provided to `useDroppableCollection`. In most cases, you can use a default implementation provided by React Aria such as` ListKeyboardDelegate `and` ListDropTargetDelegate `, but you may also provide your own if you need to customize the behavior. ## Dropping on items# * * * This example renders a ListBox using the useListBox hook, and adds support for dropping data onto items. The highlighted code sections below show the main additions for drag and drop compared with a normal listbox. import {Item, useDroppableCollectionState, useListState} from 'react-stately'; import {ListDropTargetDelegate, ListKeyboardDelegate, mergeProps, useDroppableCollection, useDroppableItem, useFocusRing, useListBox, useOption} from 'react-aria'; function ListBox(props) { // Setup listbox as normal. See the useListBox docs for more details. let state = useListState(props); let ref = React.useRef(null); let { listBoxProps } = useListBox(props, state, ref); // Setup react-stately and react-aria hooks for drag and drop. let dropState = useDroppableCollectionState({ ...props, // Collection and selection manager come from list state. collection: state.collection, selectionManager: state.selectionManager }); let { collectionProps } = useDroppableCollection( { ...props, // Provide drop targets for keyboard and pointer-based drag and drop. keyboardDelegate: new ListKeyboardDelegate( state.collection, state.disabledKeys, ref ), dropTargetDelegate: new ListDropTargetDelegate(state.collection, ref) }, dropState, ref ); // Merge listbox props and dnd props, and render the items as normal. return ( <ul {...mergeProps(listBoxProps, collectionProps)} ref={ref}> {[...state.collection].map((item) => ( <Option key={item.key} item={item} state={state} dropState={dropState} /> ))} </ul> ); } function Option({ item, state, dropState }) { // Setup listbox option as normal. See useListBox docs for details. let ref = React.useRef(null); let { optionProps } = useOption({ key: item.key }, state, ref); let { isFocusVisible, focusProps } = useFocusRing(); // Register the item as a drop target. let { dropProps, isDropTarget } = useDroppableItem( { target: { type: 'item', key: item.key, dropPosition: 'on' } }, dropState, ref ); // Merge option props and dnd props, and render the item. return ( <li {...mergeProps(optionProps, dropProps, focusProps)} ref={ref} // Apply a class when the item is the active drop target. className={`option ${isFocusVisible ? 'focus-visible' : ''} ${ isDropTarget ? 'drop-target' : '' }`} > {item.rendered} </li> ); } <Draggable>Octopus</Draggable> <ListBox aria-label="Categories" selectionMode="single" onItemDrop={(e) => alert(`Dropped on ${e.target.key}`)} > <Item key="animals">Animals</Item> <Item key="people">People</Item> <Item key="plants">Plants</Item> </ListBox> import { Item, useDroppableCollectionState, useListState } from 'react-stately'; import { ListDropTargetDelegate, ListKeyboardDelegate, mergeProps, useDroppableCollection, useDroppableItem, useFocusRing, useListBox, useOption } from 'react-aria'; function ListBox(props) { // Setup listbox as normal. See the useListBox docs for more details. let state = useListState(props); let ref = React.useRef(null); let { listBoxProps } = useListBox(props, state, ref); // Setup react-stately and react-aria hooks for drag and drop. let dropState = useDroppableCollectionState({ ...props, // Collection and selection manager come from list state. collection: state.collection, selectionManager: state.selectionManager }); let { collectionProps } = useDroppableCollection( { ...props, // Provide drop targets for keyboard and pointer-based drag and drop. keyboardDelegate: new ListKeyboardDelegate( state.collection, state.disabledKeys, ref ), dropTargetDelegate: new ListDropTargetDelegate( state.collection, ref ) }, dropState, ref ); // Merge listbox props and dnd props, and render the items as normal. return ( <ul {...mergeProps(listBoxProps, collectionProps)} ref={ref} > {[...state.collection].map((item) => ( <Option key={item.key} item={item} state={state} dropState={dropState} /> ))} </ul> ); } function Option({ item, state, dropState }) { // Setup listbox option as normal. See useListBox docs for details. let ref = React.useRef(null); let { optionProps } = useOption( { key: item.key }, state, ref ); let { isFocusVisible, focusProps } = useFocusRing(); // Register the item as a drop target. let { dropProps, isDropTarget } = useDroppableItem( { target: { type: 'item', key: item.key, dropPosition: 'on' } }, dropState, ref ); // Merge option props and dnd props, and render the item. return ( <li {...mergeProps(optionProps, dropProps, focusProps)} ref={ref} // Apply a class when the item is the active drop target. className={`option ${ isFocusVisible ? 'focus-visible' : '' } ${isDropTarget ? 'drop-target' : ''}`} > {item.rendered} </li> ); } <Draggable>Octopus</Draggable> <ListBox aria-label="Categories" selectionMode="single" onItemDrop={(e) => alert(`Dropped on ${e.target.key}`)} > <Item key="animals">Animals</Item> <Item key="people">People</Item> <Item key="plants">Plants</Item> </ListBox> import { Item, useDroppableCollectionState, useListState } from 'react-stately'; import { ListDropTargetDelegate, ListKeyboardDelegate, mergeProps, useDroppableCollection, useDroppableItem, useFocusRing, useListBox, useOption } from 'react-aria'; function ListBox(props) { // Setup listbox as normal. See the useListBox docs for more details. let state = useListState(props); let ref = React.useRef( null ); let { listBoxProps } = useListBox( props, state, ref ); // Setup react-stately and react-aria hooks for drag and drop. let dropState = useDroppableCollectionState( { ...props, // Collection and selection manager come from list state. collection: state .collection, selectionManager: state .selectionManager } ); let { collectionProps } = useDroppableCollection( { ...props, // Provide drop targets for keyboard and pointer-based drag and drop. keyboardDelegate: new ListKeyboardDelegate( state .collection, state .disabledKeys, ref ), dropTargetDelegate: new ListDropTargetDelegate( state .collection, ref ) }, dropState, ref ); // Merge listbox props and dnd props, and render the items as normal. return ( <ul {...mergeProps( listBoxProps, collectionProps )} ref={ref} > {[ ...state .collection ].map((item) => ( <Option key={item.key} item={item} state={state} dropState={dropState} /> ))} </ul> ); } function Option( { item, state, dropState } ) { // Setup listbox option as normal. See useListBox docs for details. let ref = React.useRef( null ); let { optionProps } = useOption( { key: item.key }, state, ref ); let { isFocusVisible, focusProps } = useFocusRing(); // Register the item as a drop target. let { dropProps, isDropTarget } = useDroppableItem( { target: { type: 'item', key: item.key, dropPosition: 'on' } }, dropState, ref ); // Merge option props and dnd props, and render the item. return ( <li {...mergeProps( optionProps, dropProps, focusProps )} ref={ref} // Apply a class when the item is the active drop target. className={`option ${ isFocusVisible ? 'focus-visible' : '' } ${ isDropTarget ? 'drop-target' : '' }`} > {item.rendered} </li> ); } <Draggable> Octopus </Draggable> <ListBox aria-label="Categories" selectionMode="single" onItemDrop={(e) => alert( `Dropped on ${e.target.key}` )} > <Item key="animals"> Animals </Item> <Item key="people"> People </Item> <Item key="plants"> Plants </Item> </ListBox> ≡ Octopus * Animals * People * Plants Show CSS [role=listbox] { padding: 0; margin: 5px 0; list-style: none; box-shadow: inset 0 0 0 1px gray; max-width: 250px; outline: none; min-height: 50px; overflow: auto; } [role=listbox]:empty { box-sizing: border-box; border: 1px dashed gray; box-shadow: none; } .option { padding: 3px 6px; outline: none; } .option[aria-selected=true] { background: blueviolet; color: white; } .option.focus-visible { box-shadow: inset 0 0 0 2px orange; } .option.drop-target { border-color: transparent; box-shadow: inset 0 0 0 2px var(--blue); } [role=listbox] { padding: 0; margin: 5px 0; list-style: none; box-shadow: inset 0 0 0 1px gray; max-width: 250px; outline: none; min-height: 50px; overflow: auto; } [role=listbox]:empty { box-sizing: border-box; border: 1px dashed gray; box-shadow: none; } .option { padding: 3px 6px; outline: none; } .option[aria-selected=true] { background: blueviolet; color: white; } .option.focus-visible { box-shadow: inset 0 0 0 2px orange; } .option.drop-target { border-color: transparent; box-shadow: inset 0 0 0 2px var(--blue); } [role=listbox] { padding: 0; margin: 5px 0; list-style: none; box-shadow: inset 0 0 0 1px gray; max-width: 250px; outline: none; min-height: 50px; overflow: auto; } [role=listbox]:empty { box-sizing: border-box; border: 1px dashed gray; box-shadow: none; } .option { padding: 3px 6px; outline: none; } .option[aria-selected=true] { background: blueviolet; color: white; } .option.focus-visible { box-shadow: inset 0 0 0 2px orange; } .option.drop-target { border-color: transparent; box-shadow: inset 0 0 0 2px var(--blue); } ### Draggable# The `Draggable` component used above is defined below. See useDrag for more details and documentation. Show code import {mergeProps, useButton, useDrag} from 'react-aria'; function Draggable({ children }) { let { dragProps, dragButtonProps, isDragging } = useDrag({ getAllowedDropOperations: () => ['copy'], getItems() { return [{ 'text/plain': children, 'my-app-custom-type': JSON.stringify({ message: children }) }]; } }); let ref = React.useRef(null); let { buttonProps } = useButton( { ...dragButtonProps, elementType: 'div' }, ref ); return ( <div {...mergeProps(dragProps, buttonProps)} ref={ref} className={`draggable ${isDragging ? 'dragging' : ''}`} > <span aria-hidden="true">≡</span> {children} </div> ); } import {mergeProps, useButton, useDrag} from 'react-aria'; function Draggable({ children }) { let { dragProps, dragButtonProps, isDragging } = useDrag({ getAllowedDropOperations: () => ['copy'], getItems() { return [{ 'text/plain': children, 'my-app-custom-type': JSON.stringify({ message: children }) }]; } }); let ref = React.useRef(null); let { buttonProps } = useButton({ ...dragButtonProps, elementType: 'div' }, ref); return ( <div {...mergeProps(dragProps, buttonProps)} ref={ref} className={`draggable ${ isDragging ? 'dragging' : '' }`} > <span aria-hidden="true">≡</span> {children} </div> ); } import { mergeProps, useButton, useDrag } from 'react-aria'; function Draggable( { children } ) { let { dragProps, dragButtonProps, isDragging } = useDrag({ getAllowedDropOperations: () => ['copy'], getItems() { return [{ 'text/plain': children, 'my-app-custom-type': JSON.stringify( { message: children } ) }]; } }); let ref = React.useRef( null ); let { buttonProps } = useButton({ ...dragButtonProps, elementType: 'div' }, ref); return ( <div {...mergeProps( dragProps, buttonProps )} ref={ref} className={`draggable ${ isDragging ? 'dragging' : '' }`} > <span aria-hidden="true"> ≡ </span>{' '} {children} </div> ); } Show CSS .draggable { display: inline-block; vertical-align: top; border: 1px solid gray; padding: 5px 10px; margin-right: 20px; } .draggable.dragging { opacity: 0.5; } .draggable { display: inline-block; vertical-align: top; border: 1px solid gray; padding: 5px 10px; margin-right: 20px; } .draggable.dragging { opacity: 0.5; } .draggable { display: inline-block; vertical-align: top; border: 1px solid gray; padding: 5px 10px; margin-right: 20px; } .draggable.dragging { opacity: 0.5; } ## Dropping between items# * * * To add support for dropping between items, first implement the `DropIndicator` component using the `useDropIndicator` hook. This will render a line between items indicating the insertion position. Within a listbox, these must have `role=option`, and since our listbox is rendered as a `<ul>`, they must also be `<li>` elements to ensure the accessibility and HTML semantics are correct. `useDropIndicator` returns `isHidden` when the drop indicator is not needed (e.g. if there is no drag session in progress), in which case we can return null to prevent any extra elements from being rendered to the DOM. When `isDropTarget` is true, the drop indicator is active and should be visible. Note that for accessibility, an element must always be rendered while a drag session is in progress, even when the drop indicator is not currently active, so that screen readers can navigate to it. import {useDropIndicator} from 'react-aria'; function DropIndicator(props) { let ref = React.useRef(null); let { dropIndicatorProps, isHidden, isDropTarget } = useDropIndicator( props, props.dropState, ref ); if (isHidden) { return null; } return ( <li {...dropIndicatorProps} role="option" ref={ref} className={`drop-indicator ${isDropTarget ? 'drop-target' : ''}`} /> ); } import {useDropIndicator} from 'react-aria'; function DropIndicator(props) { let ref = React.useRef(null); let { dropIndicatorProps, isHidden, isDropTarget } = useDropIndicator(props, props.dropState, ref); if (isHidden) { return null; } return ( <li {...dropIndicatorProps} role="option" ref={ref} className={`drop-indicator ${ isDropTarget ? 'drop-target' : '' }`} /> ); } import {useDropIndicator} from 'react-aria'; function DropIndicator( props ) { let ref = React.useRef( null ); let { dropIndicatorProps, isHidden, isDropTarget } = useDropIndicator( props, props.dropState, ref ); if (isHidden) { return null; } return ( <li {...dropIndicatorProps} role="option" ref={ref} className={`drop-indicator ${ isDropTarget ? 'drop-target' : '' }`} /> ); } Now that the `DropIndicator` component is implemented, we can render an instance between each item in the list. This uses the `before` drop position by default, except for after the last item in the list. function Option({ item, state, dropState }) { // ... return ( <> <DropIndicator target={{ type: 'item', key: item.key, dropPosition: 'before' }} dropState={dropState} /> <li {...mergeProps(optionProps, dropProps, focusProps)} ref={ref} className={`option ${isFocusVisible ? 'focus-visible' : ''} ${ isDropTarget ? 'drop-target' : '' }`} > {item.rendered} </li> {state.collection.getKeyAfter(item.key) == null && ( <DropIndicator target={{ type: 'item', key: item.key, dropPosition: 'after' }} dropState={dropState} /> )} </> ); } function Option({ item, state, dropState }) { // ... return ( <> <DropIndicator target={{ type: 'item', key: item.key, dropPosition: 'before' }} dropState={dropState} /> <li {...mergeProps(optionProps, dropProps, focusProps)} ref={ref} className={`option ${ isFocusVisible ? 'focus-visible' : '' } ${isDropTarget ? 'drop-target' : ''}`} > {item.rendered} </li> {state.collection.getKeyAfter(item.key) == null && ( <DropIndicator target={{ type: 'item', key: item.key, dropPosition: 'after' }} dropState={dropState} /> )} </> ); } function Option( { item, state, dropState } ) { // ... return ( <> <DropIndicator target={{ type: 'item', key: item.key, dropPosition: 'before' }} dropState={dropState} /> <li {...mergeProps( optionProps, dropProps, focusProps )} ref={ref} className={`option ${ isFocusVisible ? 'focus-visible' : '' } ${ isDropTarget ? 'drop-target' : '' }`} > {item.rendered} </li> {state.collection .getKeyAfter( item.key ) == null && ( <DropIndicator target={{ type: 'item', key: item.key, dropPosition: 'after' }} dropState={dropState} /> )} </> ); } Now, we can render an example ListBox, which inserts a new item on drop. This uses the useListData hook to manage the list of items, which is updated in the `onInsert` event. Note that `useListData` is a convenience hook, not a requirement. You can manage your state however you wish. import {useListData} from 'react-stately'; function Example() { let list = useListData({ initialItems: [ { id: 1, name: 'Cat' }, { id: 2, name: 'Dog' }, { id: 3, name: 'Kangaroo' } ] }); let onInsert = async (e) => { let name = await e.items[0].getText('text/plain'); let item = { id: list.items.length + 1, name }; if (e.target.dropPosition === 'before') { list.insertBefore(e.target.key, item); } else if (e.target.dropPosition === 'after') { list.insertAfter(e.target.key, item); } }; return ( <> <Draggable>Octopus</Draggable> <ListBox aria-label="Favorite animals" selectionMode="single" items={list.items} acceptedDragTypes={['text/plain']} onInsert={onInsert} > {(item) => <Item>{item.name}</Item>} </ListBox> </> ); } import {useListData} from 'react-stately'; function Example() { let list = useListData({ initialItems: [ { id: 1, name: 'Cat' }, { id: 2, name: 'Dog' }, { id: 3, name: 'Kangaroo' } ] }); let onInsert = async (e) => { let name = await e.items[0].getText('text/plain'); let item = { id: list.items.length + 1, name }; if (e.target.dropPosition === 'before') { list.insertBefore(e.target.key, item); } else if (e.target.dropPosition === 'after') { list.insertAfter(e.target.key, item); } }; return ( <> <Draggable>Octopus</Draggable> <ListBox aria-label="Favorite animals" selectionMode="single" items={list.items} acceptedDragTypes={['text/plain']} onInsert={onInsert} > {(item) => <Item>{item.name}</Item>} </ListBox> </> ); } import {useListData} from 'react-stately'; function Example() { let list = useListData( { initialItems: [ { id: 1, name: 'Cat' }, { id: 2, name: 'Dog' }, { id: 3, name: 'Kangaroo' } ] } ); let onInsert = async (e) => { let name = await e .items[0] .getText( 'text/plain' ); let item = { id: list.items .length + 1, name }; if ( e.target .dropPosition === 'before' ) { list .insertBefore( e.target.key, item ); } else if ( e.target .dropPosition === 'after' ) { list.insertAfter( e.target.key, item ); } }; return ( <> <Draggable> Octopus </Draggable> <ListBox aria-label="Favorite animals" selectionMode="single" items={list .items} acceptedDragTypes={[ 'text/plain' ]} onInsert={onInsert} > {(item) => ( <Item> {item.name} </Item> )} </ListBox> </> ); } ≡ Octopus * Cat * Dog * Kangaroo Show CSS .drop-indicator { width: 100%; margin-left: 0; height: 2px; margin-bottom: -2px; outline: none; background: transparent; } .drop-indicator:last-child { margin-bottom: 0; margin-top: -2px; } .drop-indicator.drop-target { background: var(--blue); } .drop-indicator { width: 100%; margin-left: 0; height: 2px; margin-bottom: -2px; outline: none; background: transparent; } .drop-indicator:last-child { margin-bottom: 0; margin-top: -2px; } .drop-indicator.drop-target { background: var(--blue); } .drop-indicator { width: 100%; margin-left: 0; height: 2px; margin-bottom: -2px; outline: none; background: transparent; } .drop-indicator:last-child { margin-bottom: 0; margin-top: -2px; } .drop-indicator.drop-target { background: var(--blue); } ## Dropping on the collection# * * * To add support for dropping on the collection as a whole, an additional `DropIndicator` can be rendered at the start of the list, representing the `root` target. The `isDropTarget` method of the state object can be used to apply a class to the list when the root target is active. The `onRootDrop` event is triggered when the user drops on the collection. This example also accepts drops on the "Documents" item, by implementing `onItemDrop` as well as `shouldAcceptItemDrop` to limit which items allow drops. function ListBox(props) { // ... let isDropTarget = dropState.isDropTarget({type: 'root'}); return ( <ul {...mergeProps(listBoxProps, collectionProps)} ref={ref} className={isDropTarget ? 'drop-target' : ''} > <DropIndicator target={{type: 'root'}} dropState={dropState} /> {[...state.collection].map((item) => ( <Option key={item.key} item={item} state={state} dropState={dropState} /> ))} </ul> ); } <Draggable>budget.xls</Draggable> <ListBox aria-label="Files" selectionMode="single" onRootDrop={() => alert('Dropped on root')} onItemDrop={e => alert(`Dropped on ${e.target.key}`)} shouldAcceptItemDrop={target => target.key === 'documents'}> <Item key="documents">Documents</Item> <Item>proposal.doc</Item> <Item>presentation.ppt</Item> </ListBox> function ListBox(props) { // ... let isDropTarget = dropState.isDropTarget({ type: 'root' }); return ( <ul {...mergeProps(listBoxProps, collectionProps)} ref={ref} className={isDropTarget ? 'drop-target' : ''} > <DropIndicator target={{ type: 'root' }} dropState={dropState} /> {[...state.collection].map((item) => ( <Option key={item.key} item={item} state={state} dropState={dropState} /> ))} </ul> ); } <Draggable>budget.xls</Draggable> <ListBox aria-label="Files" selectionMode="single" onRootDrop={() => alert('Dropped on root')} onItemDrop={(e) => alert(`Dropped on ${e.target.key}`)} shouldAcceptItemDrop={(target) => target.key === 'documents'}> <Item key="documents">Documents</Item> <Item>proposal.doc</Item> <Item>presentation.ppt</Item> </ListBox> function ListBox(props) { // ... let isDropTarget = dropState .isDropTarget({ type: 'root' }); return ( <ul {...mergeProps( listBoxProps, collectionProps )} ref={ref} className={isDropTarget ? 'drop-target' : ''} > <DropIndicator target={{ type: 'root' }} dropState={dropState} /> {[ ...state .collection ].map((item) => ( <Option key={item.key} item={item} state={state} dropState={dropState} /> ))} </ul> ); } <Draggable> budget.xls </Draggable> <ListBox aria-label="Files" selectionMode="single" onRootDrop={() => alert( 'Dropped on root' )} onItemDrop={(e) => alert( `Dropped on ${e.target.key}` )} shouldAcceptItemDrop={(target) => target.key === 'documents'}> <Item key="documents"> Documents </Item> <Item> proposal.doc </Item> <Item> presentation.ppt </Item> </ListBox> ≡ budget.xls * Documents * proposal.doc * presentation.ppt Show CSS [role=listbox].drop-target { box-shadow: inset 0 0 0 2px var(--blue); } [role=listbox].drop-target { box-shadow: inset 0 0 0 2px var(--blue); } [role=listbox].drop-target { box-shadow: inset 0 0 0 2px var(--blue); } ## Reordering# * * * Drag and drop can be combined in the same collection component to allow reordering items. This example builds on the dropping between items example above to add support for dragging items as well. This is done using the `useDraggableCollection` and `useDraggableItem` hooks. See the docs for more details on these hooks. import {useDraggableCollection, useDraggableItem} from 'react-aria'; import {useDraggableCollectionState} from 'react-stately'; function ReorderableListBox(props) { // See useListBox docs for more details. let state = useListState(props); let ref = React.useRef(null); let { listBoxProps } = useListBox( { ...props, shouldSelectOnPressUp: true }, state, ref ); let dropState = useDroppableCollectionState({ ...props, collection: state.collection, selectionManager: state.selectionManager }); let { collectionProps } = useDroppableCollection( { ...props, keyboardDelegate: new ListKeyboardDelegate( state.collection, state.disabledKeys, ref ), dropTargetDelegate: new ListDropTargetDelegate(state.collection, ref) }, dropState, ref ); // Setup drag state for the collection. let dragState = useDraggableCollectionState({ ...props, // Collection and selection manager come from list state. collection: state.collection, selectionManager: state.selectionManager, // Provide data for each dragged item. This function could // also be provided by the user of the component. getItems: props.getItems || ((keys) => { return [...keys].map((key) => { let item = state.collection.getItem(key); return { 'text/plain': item.textValue }; }); }) }); useDraggableCollection(props, dragState, ref); return ( <ul {...mergeProps(listBoxProps, collectionProps)} ref={ref} > {[...state.collection].map((item) => ( <ReorderableOption key={item.key} item={item} state={state} dragState={dragState} dropState={dropState} /> ))} </ul> ); } function ReorderableOption({ item, state, dragState, dropState }) { // ... // Register the item as a drag source. let { dragProps } = useDraggableItem({ key: item.key }, dragState); return ( <> <DropIndicator target={{ type: 'item', key: item.key, dropPosition: 'before' }} dropState={dropState} /> <li ...mergeProps(optionProps, dragProps, dropProps, focusProps)} ref={ref} className={`option ${isFocusVisible ? 'focus-visible' : ''} ${ isDropTarget ? 'drop-target' : '' }`} > {item.rendered} </li> {state.collection.getKeyAfter(item.key) == null && ( <DropIndicator target={{ type: 'item', key: item.key, dropPosition: 'after' }} dropState={dropState} /> )} </> ); } import { useDraggableCollection, useDraggableItem } from 'react-aria'; import {useDraggableCollectionState} from 'react-stately'; function ReorderableListBox(props) { // See useListBox docs for more details. let state = useListState(props); let ref = React.useRef(null); let { listBoxProps } = useListBox( { ...props, shouldSelectOnPressUp: true }, state, ref ); let dropState = useDroppableCollectionState({ ...props, collection: state.collection, selectionManager: state.selectionManager }); let { collectionProps } = useDroppableCollection( { ...props, keyboardDelegate: new ListKeyboardDelegate( state.collection, state.disabledKeys, ref ), dropTargetDelegate: new ListDropTargetDelegate( state.collection, ref ) }, dropState, ref ); // Setup drag state for the collection. let dragState = useDraggableCollectionState({ ...props, // Collection and selection manager come from list state. collection: state.collection, selectionManager: state.selectionManager, // Provide data for each dragged item. This function could // also be provided by the user of the component. getItems: props.getItems || ((keys) => { return [...keys].map((key) => { let item = state.collection.getItem(key); return { 'text/plain': item.textValue }; }); }) }); useDraggableCollection(props, dragState, ref); return ( <ul {...mergeProps(listBoxProps, collectionProps)} ref={ref} > {[...state.collection].map((item) => ( <ReorderableOption key={item.key} item={item} state={state} dragState={dragState} dropState={dropState} /> ))} </ul> ); } function ReorderableOption( { item, state, dragState, dropState } ) { // ... // Register the item as a drag source. let { dragProps } = useDraggableItem({ key: item.key }, dragState); return ( <> <DropIndicator target={{ type: 'item', key: item.key, dropPosition: 'before' }} dropState={dropState} /> <li ...mergeProps( optionProps, dragProps, dropProps, focusProps )} ref={ref} className={`option ${ isFocusVisible ? 'focus-visible' : '' } ${isDropTarget ? 'drop-target' : ''}`} > {item.rendered} </li> {state.collection.getKeyAfter(item.key) == null && ( <DropIndicator target={{ type: 'item', key: item.key, dropPosition: 'after' }} dropState={dropState} /> )} </> ); } import { useDraggableCollection, useDraggableItem } from 'react-aria'; import {useDraggableCollectionState} from 'react-stately'; function ReorderableListBox( props ) { // See useListBox docs for more details. let state = useListState(props); let ref = React.useRef( null ); let { listBoxProps } = useListBox( { ...props, shouldSelectOnPressUp: true }, state, ref ); let dropState = useDroppableCollectionState( { ...props, collection: state .collection, selectionManager: state .selectionManager } ); let { collectionProps } = useDroppableCollection( { ...props, keyboardDelegate: new ListKeyboardDelegate( state .collection, state .disabledKeys, ref ), dropTargetDelegate: new ListDropTargetDelegate( state .collection, ref ) }, dropState, ref ); // Setup drag state for the collection. let dragState = useDraggableCollectionState( { ...props, // Collection and selection manager come from list state. collection: state .collection, selectionManager: state .selectionManager, // Provide data for each dragged item. This function could // also be provided by the user of the component. getItems: props .getItems || ((keys) => { return [ ...keys ].map( (key) => { let item = state .collection .getItem( key ); return { 'text/plain': item .textValue }; } ); }) } ); useDraggableCollection( props, dragState, ref ); return ( <ul {...mergeProps( listBoxProps, collectionProps )} ref={ref} > {[ ...state .collection ].map((item) => ( <ReorderableOption key={item.key} item={item} state={state} dragState={dragState} dropState={dropState} /> ))} </ul> ); } function ReorderableOption( { item, state, dragState, dropState } ) { // ... // Register the item as a drag source. let { dragProps } = useDraggableItem({ key: item.key }, dragState); return ( <> <DropIndicator target={{ type: 'item', key: item.key, dropPosition: 'before' }} dropState={dropState} /> <li ...mergeProps( optionProps, dragProps, dropProps, focusProps )} ref={ref} className={`option ${ isFocusVisible ? 'focus-visible' : '' } ${ isDropTarget ? 'drop-target' : '' }`} > {item.rendered} </li> {state.collection .getKeyAfter( item.key ) == null && ( <DropIndicator target={{ type: 'item', key: item.key, dropPosition: 'after' }} dropState={dropState} /> )} </> ); } Now, we can render an example ListBox, which allows the user to reorder items. The `onReorder` event is triggered when the user drops dragged items which originated within the same collection. As above, useListData is used to manage the list items in this example, but it is not a requirement. import {useListData} from 'react-stately'; function Example() { let list = useListData({ initialItems: [ { id: 1, name: 'Cat' }, { id: 2, name: 'Dog' }, { id: 3, name: 'Kangaroo' }, { id: 4, name: 'Panda' }, { id: 5, name: 'Snake' } ] }); let onReorder = (e) => { if (e.target.dropPosition === 'before') { list.moveBefore(e.target.key, e.keys); } else if (e.target.dropPosition === 'after') { list.moveAfter(e.target.key, e.keys); } }; return ( <ReorderableListBox aria-label="Favorite animals" selectionMode="multiple" selectionBehavior="replace" items={list.items} onReorder={onReorder} > {(item) => <Item>{item.name}</Item>} </ReorderableListBox> ); } import {useListData} from 'react-stately'; function Example() { let list = useListData({ initialItems: [ { id: 1, name: 'Cat' }, { id: 2, name: 'Dog' }, { id: 3, name: 'Kangaroo' }, { id: 4, name: 'Panda' }, { id: 5, name: 'Snake' } ] }); let onReorder = (e) => { if (e.target.dropPosition === 'before') { list.moveBefore(e.target.key, e.keys); } else if (e.target.dropPosition === 'after') { list.moveAfter(e.target.key, e.keys); } }; return ( <ReorderableListBox aria-label="Favorite animals" selectionMode="multiple" selectionBehavior="replace" items={list.items} onReorder={onReorder} > {(item) => <Item>{item.name}</Item>} </ReorderableListBox> ); } import {useListData} from 'react-stately'; function Example() { let list = useListData( { initialItems: [ { id: 1, name: 'Cat' }, { id: 2, name: 'Dog' }, { id: 3, name: 'Kangaroo' }, { id: 4, name: 'Panda' }, { id: 5, name: 'Snake' } ] } ); let onReorder = (e) => { if ( e.target .dropPosition === 'before' ) { list.moveBefore( e.target.key, e.keys ); } else if ( e.target .dropPosition === 'after' ) { list.moveAfter( e.target.key, e.keys ); } }; return ( <ReorderableListBox aria-label="Favorite animals" selectionMode="multiple" selectionBehavior="replace" items={list.items} onReorder={onReorder} > {(item) => ( <Item> {item.name} </Item> )} </ReorderableListBox> ); } * Cat * Dog * Kangaroo * Panda * Snake ## Drop data# * * * `useDroppableCollection` allows users to drop one or more **drag items**, each of which contains data to be transferred from the drag source to drop target. There are three kinds of drag items: * `text` – represents data inline as a string in one or more formats * `file` – references a file on the user's device * `directory` – references the contents of a directory ### Text# A `TextDropItem` represents textual data in one or more different formats. These may be either standard mime types or custom app-specific formats. Representing data in multiple formats allows drop targets both within and outside an application to choose data in a format that they understand. For example, a complex object may be serialized in a custom format for use within an application, with fallbacks in plain text and/or rich HTML that can be used when a user drops data from an external application. The example below uses the `acceptedDragTypes` prop to accept items that include a custom app-specific type, which is retrieved using the item's `getText` method. The same draggable component as used in the above example is used here, but rather than displaying the plain text representation, the custom format is used instead. When `acceptedDragTypes` is specified, the dropped items are filtered to include only items that include the accepted types. function Example() { let list = useListData({ initialItems: [ {id: 1, name: 'Cat'}, {id: 2, name: 'Dog'}, {id: 3, name: 'Kangaroo'} ] }); let onInsert = async e => { let value = JSON.parse(await e.items[0].getText('my-app-custom-type')); let item = {id: list.items.length + 1, name: value.message}; if (e.target.dropPosition === 'before') { list.insertBefore(e.target.key, item); } else if (e.target.dropPosition === 'after') { list.insertAfter(e.target.key, item); } }; return ( <> <Draggable>Octopus</Draggable> <ListBox aria-label="Favorite animals" selectionMode="single" items={list.items} acceptedDragTypes={['my-app-custom-type']} onInsert={onInsert}> {item => <Item>{item.name}</Item>} </ListBox> </> ); } function Example() { let list = useListData({ initialItems: [ { id: 1, name: 'Cat' }, { id: 2, name: 'Dog' }, { id: 3, name: 'Kangaroo' } ] }); let onInsert = async (e) => { let value = JSON.parse( await e.items[0].getText('my-app-custom-type') ); let item = { id: list.items.length + 1, name: value.message }; if (e.target.dropPosition === 'before') { list.insertBefore(e.target.key, item); } else if (e.target.dropPosition === 'after') { list.insertAfter(e.target.key, item); } }; return ( <> <Draggable>Octopus</Draggable> <ListBox aria-label="Favorite animals" selectionMode="single" items={list.items} acceptedDragTypes={['my-app-custom-type']} onInsert={onInsert} > {(item) => <Item>{item.name}</Item>} </ListBox> </> ); } function Example() { let list = useListData( { initialItems: [ { id: 1, name: 'Cat' }, { id: 2, name: 'Dog' }, { id: 3, name: 'Kangaroo' } ] } ); let onInsert = async (e) => { let value = JSON .parse( await e .items[0] .getText( 'my-app-custom-type' ) ); let item = { id: list.items .length + 1, name: value.message }; if ( e.target .dropPosition === 'before' ) { list .insertBefore( e.target.key, item ); } else if ( e.target .dropPosition === 'after' ) { list.insertAfter( e.target.key, item ); } }; return ( <> <Draggable> Octopus </Draggable> <ListBox aria-label="Favorite animals" selectionMode="single" items={list .items} acceptedDragTypes={[ 'my-app-custom-type' ]} onInsert={onInsert} > {(item) => ( <Item> {item.name} </Item> )} </ListBox> </> ); } ≡ Octopus * Cat * Dog * Kangaroo ### Files# A `FileDropItem` references a file on the user's device. It includes the name and mime type of the file, and methods to read the contents as plain text, or retrieve a native File object which can be attached to form data for uploading. This example accepts JPEG and PNG image files, and renders them by creating a local object URL. When the list is empty, you can drop on the whole collection, and otherwise items can be inserted. function Example() { let list = useListData({}); let getItems = e => { return Promise.all( e.items.map(async item => ({ id: Math.random(), url: URL.createObjectURL(await item.getFile()), name: item.name })) ); }; let onRootDrop = async e => { list.prepend(...await getItems(e)); }; let onInsert = async e => { if (e.target.dropPosition === 'before') { list.insertBefore(e.target.key, ...await getItems(e)); } else if (e.target.dropPosition === 'after') { list.insertAfter(e.target.key, ...await getItems(e)); } }; return ( <ListBox aria-label="Images" items={list.items} acceptedDragTypes={['image/jpeg', 'image/png']} onRootDrop={onRootDrop} onInsert={onInsert}> {item => ( <Item textValue={item.name}> <div className="image-item"> <img src={item.url} /> <span>{item.name}</span> </div> </Item> )} </ListBox> ); } function Example() { let list = useListData({}); let getItems = e => { return Promise.all( e.items.map(async item => ({ id: Math.random(), url: URL.createObjectURL(await item.getFile()), name: item.name })) ); }; let onRootDrop = async e => { list.prepend(...await getItems(e)); }; let onInsert = async e => { if (e.target.dropPosition === 'before') { list.insertBefore(e.target.key, ...await getItems(e)); } else if (e.target.dropPosition === 'after') { list.insertAfter(e.target.key, ...await getItems(e)); } }; return ( <ListBox aria-label="Images" items={list.items} acceptedDragTypes={['image/jpeg', 'image/png']} onRootDrop={onRootDrop} onInsert={onInsert}> {item => ( <Item textValue={item.name}> <div className="image-item"> <img src={item.url} /> <span>{item.name}</span> </div> </Item> )} </ListBox> ); } function Example() { let list = useListData( {} ); let getItems = (e) => { return Promise.all( e.items.map( async (item) => ({ id: Math .random(), url: URL .createObjectURL( await item .getFile() ), name: item.name }) ) ); }; let onRootDrop = async (e) => { list.prepend( ...await getItems( e ) ); }; let onInsert = async (e) => { if ( e.target .dropPosition === 'before' ) { list .insertBefore( e.target.key, ...await getItems( e ) ); } else if ( e.target .dropPosition === 'after' ) { list.insertAfter( e.target.key, ...await getItems( e ) ); } }; return ( <ListBox aria-label="Images" items={list.items} acceptedDragTypes={[ 'image/jpeg', 'image/png' ]} onRootDrop={onRootDrop} onInsert={onInsert} > {(item) => ( <Item textValue={item .name} > <div className="image-item"> <img src={item .url} /> <span> {item.name} </span> </div> </Item> )} </ListBox> ); } Show CSS .image-item { display: flex; height: 50px; gap: 10px; } .image-item img { height: 100%; object-fit: contain; } .image-item span { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .image-item { display: flex; height: 50px; gap: 10px; } .image-item img { height: 100%; object-fit: contain; } .image-item span { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .image-item { display: flex; height: 50px; gap: 10px; } .image-item img { height: 100%; object-fit: contain; } .image-item span { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } ### Directories# A `DirectoryDropItem` references the contents of a directory on the user's device. It includes the name of the directory, as well as a method to iterate through the files and folders within the directory. The contents of any folders within the directory can be accessed recursively. The `getEntries` method returns an async iterable object, which can be used in a `for await...of` loop. This provides each item in the directory as either a `FileDropItem` or` DirectoryDropItem `, and you can access the contents of each file as discussed above. This example accepts directory drops over the whole collection, and renders the contents as items in the list. `DIRECTORY_DRAG_TYPE` is imported from `@react-aria/dnd` and included in the `acceptedDragTypes` prop to limit the accepted items to only directories. import {DIRECTORY_DRAG_TYPE} from 'react-aria'; import File from '@spectrum-icons/workflow/FileTxt'; import Folder from '@spectrum-icons/workflow/Folder'; function Example() { let [files, setFiles] = React.useState([]); let onRootDrop = async (e) => { // Read entries in directory and update state with relevant info. let files = []; for await (let entry of e.items[0].getEntries()) { files.push({ name: entry.name, kind: entry.kind }); } setFiles(files); }; return ( <ListBox aria-label="Directory contents" items={files} acceptedDragTypes={[DIRECTORY_DRAG_TYPE]} onRootDrop={onRootDrop} > {(item) => ( <Item key={item.name} textValue={item.name}> <div className="dir-item"> {item.kind === 'directory' ? <Folder /> : <File />} <span>{item.name}</span> </div> </Item> )} </ListBox> ); } import {DIRECTORY_DRAG_TYPE} from 'react-aria'; import File from '@spectrum-icons/workflow/FileTxt'; import Folder from '@spectrum-icons/workflow/Folder'; function Example() { let [files, setFiles] = React.useState([]); let onRootDrop = async (e) => { // Read entries in directory and update state with relevant info. let files = []; for await (let entry of e.items[0].getEntries()) { files.push({ name: entry.name, kind: entry.kind }); } setFiles(files); }; return ( <ListBox aria-label="Directory contents" items={files} acceptedDragTypes={[DIRECTORY_DRAG_TYPE]} onRootDrop={onRootDrop} > {(item) => ( <Item key={item.name} textValue={item.name}> <div className="dir-item"> {item.kind === 'directory' ? <Folder /> : <File />} <span>{item.name}</span> </div> </Item> )} </ListBox> ); } import {DIRECTORY_DRAG_TYPE} from 'react-aria'; import File from '@spectrum-icons/workflow/FileTxt'; import Folder from '@spectrum-icons/workflow/Folder'; function Example() { let [files, setFiles] = React.useState([]); let onRootDrop = async (e) => { // Read entries in directory and update state with relevant info. let files = []; for await ( let entry of e .items[0] .getEntries() ) { files.push({ name: entry.name, kind: entry.kind }); } setFiles(files); }; return ( <ListBox aria-label="Directory contents" items={files} acceptedDragTypes={[ DIRECTORY_DRAG_TYPE ]} onRootDrop={onRootDrop} > {(item) => ( <Item key={item.name} textValue={item .name} > <div className="dir-item"> {item .kind === 'directory' ? ( <Folder /> ) : <File />} <span> {item.name} </span> </div> </Item> )} </ListBox> ); } Show CSS .dir-item { display: flex; align-items: center; gap: 8px; } .dir-item { flex: 0 0 auto; } .dir-item { flex: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .dir-item { display: flex; align-items: center; gap: 8px; } .dir-item { flex: 0 0 auto; } .dir-item { flex: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .dir-item { display: flex; align-items: center; gap: 8px; } .dir-item { flex: 0 0 auto; } .dir-item { flex: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } ## Drop operations# * * * A `DropOperation` is an indication of what will happen when dragged data is dropped on a particular drop target. These are: * `move` – indicates that the dragged data will be moved from its source location to the target location. * `copy` – indicates that the dragged data will be copied to the target destination. * `link` – indicates that there will be a relationship established between the source and target locations. * `cancel` – indicates that the drag and drop operation will be canceled, resulting in no changes made to the source or target. Many operating systems display these in the form of a cursor change, e.g. a plus sign to indicate a copy operation. The user may also be able to use a modifier key to choose which drop operation to perform, such as Option or Alt to switch from move to copy. The drag source can specify which drop operations are allowed for the dragged data (see the useDrag docs for how to customize this). By default, the first allowed operation is allowed by drop targets, meaning that the drop target accepts data of any type and operation. ### getDropOperation# The `getDropOperation` function passed to `useDroppableCollection` can be used to provide appropriate feedback to the user when a drag hovers over the drop target. This function receives the drop target, set of types contained in the drag, and a list of allowed drop operations as specified by the drag source. It should return one of the drop operations in `allowedOperations`, or a specific drop operation if only that drop operation is supported. It may also return `'cancel'` to reject the drop. If the returned operation is not in `allowedOperations`, then the drop target will act as if `'cancel'` was returned. In the below example, the drop target only supports dropping PNG images. If a PNG is dragged over the target, it will be highlighted and the operating system displays a copy cursor. If another type is dragged over the target, then there is no visual feedback, indicating that a drop is not accepted there. If the user holds a modifier key such as Control while dragging over the drop target in order to change the drop operation, then the drop target does not accept the drop. function Example() { // ... return ( <ListBox aria-label="Images" items={list.items} getDropOperation={() => 'copy'} acceptedDragTypes={['image/png']} onRootDrop={onRootDrop}> {item => ( <Item textValue={item.name}> <div className="image-item"> <img src={item.url} /> <span>{item.name}</span> </div> </Item> )} </ListBox> ); } function Example() { // ... return ( <ListBox aria-label="Images" items={list.items} getDropOperation={() => 'copy'} acceptedDragTypes={['image/png']} onRootDrop={onRootDrop}> {item => ( <Item textValue={item.name}> <div className="image-item"> <img src={item.url} /> <span>{item.name}</span> </div> </Item> )} </ListBox> ); } function Example() { // ... return ( <ListBox aria-label="Images" items={list.items} getDropOperation={() => 'copy'} acceptedDragTypes={[ 'image/png' ]} onRootDrop={onRootDrop} > {(item) => ( <Item textValue={item .name} > <div className="image-item"> <img src={item .url} /> <span> {item.name} </span> </div> </Item> )} </ListBox> ); } ### Drop events# Drop events such as `onInsert`, `onItemDrop`, etc. also include the `dropOperation`. This can be used to perform different actions accordingly, for example, when communicating with a backend API. let onItemDrop = async (e) => { let data = JSON.parse(await e.items[0].getText('my-app-file')); switch (e.dropOperation) { case 'move': MyAppFileService.move(data.filePath, props.filePath); break; case 'copy': MyAppFileService.copy(data.filePath, props.filePath); break; case 'link': MyAppFileService.link(data.filePath, props.filePath); break; }}; let onItemDrop = async (e) => { let data = JSON.parse( await e.items[0].getText('my-app-file') ); switch (e.dropOperation) { case 'move': MyAppFileService.move(data.filePath, props.filePath); break; case 'copy': MyAppFileService.copy(data.filePath, props.filePath); break; case 'link': MyAppFileService.link(data.filePath, props.filePath); break; }}; let onItemDrop = async ( e ) => { let data = JSON.parse( await e.items[0] .getText( 'my-app-file' ) ); switch ( e.dropOperation ) { case 'move': MyAppFileService .move( data.filePath, props.filePath ); break; case 'copy': MyAppFileService .copy( data.filePath, props.filePath ); break; case 'link': MyAppFileService .link( data.filePath, props.filePath ); break; }}; ## Low level API# * * * The above examples have used high level events such as `onInsert`, `onItemDrop`, and `onReorder`, along with props such as `acceptedDragTypes`. Based on these props, `useDroppableCollection` automatically determines whether drag data is accepted and where (e.g. on items, between items, etc.). It also automatically filters the dropped items based on their types. For more complex scenarios, the lower level `getDropOperation` and `onDrop` functions can be used instead. To programmatically determine whether a drop is accepted based on the dragged types, target key, and drop position, implement the `getDropOperation` function. `acceptedDragTypes` and `shouldAcceptItemDrop` (when `onItemDrop` is provided) may also be used in combination with `getDropOperation` as a pre-filter. See the section on drop operations above for more details. To handle all accepted drops in a single function, implement the `onDrop` event rather than `onInsert`, `onItemDrop`, etc. When defined, this overrides any other drop handlers. The provided `DroppableCollectionDropEvent` includes details on the drop target, dropped items, drop operation, etc. This example allows directories to be dropped between items, and only files of certain types to be dropped on the pre-existing directories. import {DIRECTORY_DRAG_TYPE} from 'react-aria'; function Example() { let list = useListData({ initialItems: [ { id: 1, name: 'Images', contains: 0, accept: ['image/png', 'image/jpeg'] }, { id: 2, name: 'Videos', contains: 0, accept: ['video/mp4'] }, { id: 3, name: 'Documents', contains: 0, accept: ['text/plain', 'application/pdf'] } ] }); let getDropOperation = (target, types, allowedOperations) => { // When dropping on an item, check whether the item accepts the drag types and cancel if not. if (target.dropPosition === 'on') { let item = list.getItem(target.key); return item.accept && item.accept.some((type) => types.has(type)) ? allowedOperations[0] : 'cancel'; } // If dropping between items, support a copy operation. return types.has(DIRECTORY_DRAG_TYPE) ? 'copy' : 'cancel'; }; let onDrop = async (e) => { let items = await Promise.all( e.items .filter((item) => { // Check if dropped item is accepted. if (e.target.dropPosition === 'on') { let folder = list.getItem(e.target.key); return folder.accept.includes(item.type); } return item.kind === 'directory'; }) .map(async (item) => { // Collect child count from dropped directories. let contains = 0; if (item.kind === 'directory') { for await (let _ of item.getEntries()) { contains++; } } return { id: Math.random(), name: item.name, contains }; }) ); // Update item count if dropping on an item, otherwise insert the new items in the list. if (e.target.dropPosition === 'on') { let item = list.getItem(e.target.key); list.update(e.target.key, { ...item, contains: item.contains + items.length }); } else if (e.target.dropPosition === 'before') { list.insertBefore(e.target.key, ...items); } else if (e.target.dropPosition === 'after') { list.insertAfter(e.target.key, ...items); } }; return ( <ListBox aria-label="Folders" items={list.items} getDropOperation={getDropOperation} onDrop={onDrop} > {(item) => ( <Item textValue={item.name}> <div className="dir-item"> <Folder /> <span>{item.name} ({item.contains} items)</span> </div> </Item> )} </ListBox> ); } import {DIRECTORY_DRAG_TYPE} from 'react-aria'; function Example() { let list = useListData({ initialItems: [ { id: 1, name: 'Images', contains: 0, accept: ['image/png', 'image/jpeg'] }, { id: 2, name: 'Videos', contains: 0, accept: ['video/mp4'] }, { id: 3, name: 'Documents', contains: 0, accept: ['text/plain', 'application/pdf'] } ] }); let getDropOperation = ( target, types, allowedOperations ) => { // When dropping on an item, check whether the item accepts the drag types and cancel if not. if (target.dropPosition === 'on') { let item = list.getItem(target.key); return item.accept && item.accept.some((type) => types.has(type)) ? allowedOperations[0] : 'cancel'; } // If dropping between items, support a copy operation. return types.has(DIRECTORY_DRAG_TYPE) ? 'copy' : 'cancel'; }; let onDrop = async (e) => { let items = await Promise.all( e.items .filter((item) => { // Check if dropped item is accepted. if (e.target.dropPosition === 'on') { let folder = list.getItem(e.target.key); return folder.accept.includes(item.type); } return item.kind === 'directory'; }) .map(async (item) => { // Collect child count from dropped directories. let contains = 0; if (item.kind === 'directory') { for await (let _ of item.getEntries()) { contains++; } } return { id: Math.random(), name: item.name, contains }; }) ); // Update item count if dropping on an item, otherwise insert the new items in the list. if (e.target.dropPosition === 'on') { let item = list.getItem(e.target.key); list.update(e.target.key, { ...item, contains: item.contains + items.length }); } else if (e.target.dropPosition === 'before') { list.insertBefore(e.target.key, ...items); } else if (e.target.dropPosition === 'after') { list.insertAfter(e.target.key, ...items); } }; return ( <ListBox aria-label="Folders" items={list.items} getDropOperation={getDropOperation} onDrop={onDrop} > {(item) => ( <Item textValue={item.name}> <div className="dir-item"> <Folder /> <span>{item.name} ({item.contains} items)</span> </div> </Item> )} </ListBox> ); } import {DIRECTORY_DRAG_TYPE} from 'react-aria'; function Example() { let list = useListData( { initialItems: [ { id: 1, name: 'Images', contains: 0, accept: [ 'image/png', 'image/jpeg' ] }, { id: 2, name: 'Videos', contains: 0, accept: [ 'video/mp4' ] }, { id: 3, name: 'Documents', contains: 0, accept: [ 'text/plain', 'application/pdf' ] } ] } ); let getDropOperation = ( target, types, allowedOperations ) => { // When dropping on an item, check whether the item accepts the drag types and cancel if not. if ( target .dropPosition === 'on' ) { let item = list .getItem( target.key ); return item .accept && item.accept .some( (type) => types .has( type ) ) ? allowedOperations[ 0 ] : 'cancel'; } // If dropping between items, support a copy operation. return types.has( DIRECTORY_DRAG_TYPE ) ? 'copy' : 'cancel'; }; let onDrop = async (e) => { let items = await Promise .all( e.items .filter( (item) => { // Check if dropped item is accepted. if ( e.target .dropPosition === 'on' ) { let folder = list .getItem( e.target .key ); return folder .accept .includes( item .type ); } return item .kind === 'directory'; } ) .map( async (item) => { // Collect child count from dropped directories. let contains = 0; if ( item .kind === 'directory' ) { for await ( let _ of item .getEntries() ) { contains++; } } return { id: Math .random(), name: item .name, contains }; } ) ); // Update item count if dropping on an item, otherwise insert the new items in the list. if ( e.target .dropPosition === 'on' ) { let item = list .getItem( e.target.key ); list.update( e.target.key, { ...item, contains: item .contains + items .length } ); } else if ( e.target .dropPosition === 'before' ) { list .insertBefore( e.target.key, ...items ); } else if ( e.target .dropPosition === 'after' ) { list.insertAfter( e.target.key, ...items ); } }; return ( <ListBox aria-label="Folders" items={list.items} getDropOperation={getDropOperation} onDrop={onDrop} > {(item) => ( <Item textValue={item .name} > <div className="dir-item"> <Folder /> <span> {item.name} {' '} ({item .contains} {' '} items) </span> </div> </Item> )} </ListBox> ); } * Images (0 items) * Videos (0 items) * Documents (0 items) ## Props# * * * The full list of props supported by droppable collections is available below. | Name | Type | Default | Description | | --- | --- | --- | --- | | `acceptedDragTypes` | `'all' | Array<string | symbol>` | `'all'` | The drag types that the droppable collection accepts. If the collection accepts directories, include `DIRECTORY_DRAG_TYPE` in your array of allowed types. | | `onInsert` | `( (e: DroppableCollectionInsertDropEvent )) => void` | — | Handler that is called when external items are dropped "between" items. | | `onRootDrop` | `( (e: DroppableCollectionRootDropEvent )) => void` | — | Handler that is called when external items are dropped on the droppable collection's root. | | `onItemDrop` | `( (e: DroppableCollectionOnItemDropEvent )) => void` | — | Handler that is called when items are dropped "on" an item. | | `onReorder` | `( (e: DroppableCollectionReorderEvent )) => void` | — | Handler that is called when items are reordered within the collection. This handler only allows dropping between items, not on items. It does not allow moving items to a different parent item within a tree. | | `onMove` | `( (e: DroppableCollectionReorderEvent )) => void` | — | Handler that is called when items are moved within the source collection. This handler allows dropping both on or between items, and items may be moved to a different parent item within a tree. | | `shouldAcceptItemDrop` | `( (target: ItemDropTarget , , types: DragTypes )) => boolean` | — | A function returning whether a given target in the droppable collection is a valid "on" drop target for the current drag types. | | `onDropEnter` | `( (e: DroppableCollectionEnterEvent )) => void` | — | Handler that is called when a valid drag enters a drop target. | | `onDropActivate` | `( (e: DroppableCollectionActivateEvent )) => void` | — | Handler that is called after a valid drag is held over a drop target for a period of time. | | `onDropExit` | `( (e: DroppableCollectionExitEvent )) => void` | — | Handler that is called when a valid drag exits a drop target. | | `onDrop` | `( (e: DroppableCollectionDropEvent )) => void` | — | Handler that is called when a valid drag is dropped on a drop target. When defined, this overrides other drop handlers such as `onInsert`, and `onItemDrop`. | | `getDropOperation` | `( target: DropTarget , types: DragTypes , allowedOperations: DropOperation [] ) => DropOperation ` | — | A function returning the drop operation to be performed when items matching the given types are dropped on the drop target. | --- ## Page: https://react-spectrum.adobe.com/react-aria/FocusRing.html # FocusRing A utility component that applies a CSS class when an element has keyboard focus. Focus rings are visible only when the user is interacting with a keyboard, not with a mouse, touch, or other input methods. <table><tbody><tr><th>install</th><td><code>yarn add react-aria</code></td></tr><tr><th>version</th><td>3.41.1</td></tr><tr><th>usage</th><td><code><span>import</span> {FocusRing} <span>from</span> <span>'react-aria'</span></code></td></tr></tbody></table> View repository GitHub View package NPM ## Introduction# * * * `FocusRing` is a utility component that can be used to apply a CSS class when an element has keyboard focus. This helps keyboard users determine which element on a page or in an application has keyboard focus as they navigate around. Focus rings are only visible when interacting with a keyboard so as not to distract mouse and touch screen users. When we are unable to detect if the user is using a mouse or touch screen, such as switching in from a different tab, we show the focus ring. If CSS classes are not being used for styling, see useFocusRing for a hooks version. ## Props# * * * | Name | Type | Default | Description | | --- | --- | --- | --- | | `children` | `ReactElement` | — | Child element to apply CSS classes to. | | `focusClass` | `string` | — | CSS class to apply when the element is focused. | | `focusRingClass` | `string` | — | CSS class to apply when the element has keyboard focus. | | `within` | `boolean` | `false` | Whether to show the focus ring when something inside the container element has focus (true), or only if the container itself has focus (false). | | `isTextInput` | `boolean` | — | Whether the element is a text input. | | `autoFocus` | `boolean` | — | Whether the element will be auto focused. | ## Example# * * * This example shows how to use `<FocusRing>` to apply a CSS class when keyboard focus is on a button. .button { -webkit-appearance: none; appearance: none; background: green; border: none; color: white; font-size: 14px; padding: 4px 8px; } .button.focus-ring { outline: 2px solid dodgerblue; outline-offset: 2px; } .button { -webkit-appearance: none; appearance: none; background: green; border: none; color: white; font-size: 14px; padding: 4px 8px; } .button.focus-ring { outline: 2px solid dodgerblue; outline-offset: 2px; } .button { -webkit-appearance: none; appearance: none; background: green; border: none; color: white; font-size: 14px; padding: 4px 8px; } .button.focus-ring { outline: 2px solid dodgerblue; outline-offset: 2px; } import {FocusRing} from 'react-aria'; <FocusRing focusRingClass="focus-ring"> <button className="button">Test</button> </FocusRing> import {FocusRing} from 'react-aria'; <FocusRing focusRingClass="focus-ring"> <button className="button">Test</button> </FocusRing> import {FocusRing} from 'react-aria'; <FocusRing focusRingClass="focus-ring"> <button className="button"> Test </button> </FocusRing> Test --- ## Page: https://react-spectrum.adobe.com/react-aria/FocusScope.html # FocusScope A FocusScope manages focus for its descendants. It supports containing focus inside the scope, restoring focus to the previously focused element on unmount, and auto focusing children on mount. It also acts as a container for a programmatic focus management interface that can be used to move focus forward and back in response to user events. <table><tbody><tr><th>install</th><td><code>yarn add react-aria</code></td></tr><tr><th>version</th><td>3.41.1</td></tr><tr><th>usage</th><td><code><span>import</span> {FocusScope, useFocusManager} <span>from</span> <span>'react-aria'</span></code></td></tr></tbody></table> View repository GitHub View package NPM ## Introduction# * * * `FocusScope` is a utility component that can be used to manage focus for its descendants. When the `contain` prop is set, focus is contained within the scope. This is useful when implementing overlays like modal dialogs, which should not allow focus to escape them while open. In addition, the `restoreFocus` prop can be used to restore focus back to the previously focused element when the focus scope unmounts, for example, back to a button which opened a dialog. A FocusScope can also optionally auto focus the first focusable element within it on mount when the `autoFocus` prop is set. The `useFocusManager` hook can also be used in combination with a FocusScope to programmatically move focus within the scope. For example, arrow key navigation could be implemented by handling keyboard events and using a focus manager to move focus to the next and previous elements. ## Props# * * * | Name | Type | Description | | --- | --- | --- | | `children` | `ReactNode` | The contents of the focus scope. | | `contain` | `boolean` | Whether to contain focus inside the scope, so users cannot move focus outside, for example in a modal dialog. | | `restoreFocus` | `boolean` | Whether to restore focus back to the element that was focused when the focus scope mounted, after the focus scope unmounts. | | `autoFocus` | `boolean` | Whether to auto focus the first focusable element in the focus scope on mount. | ## FocusManager Interface# * * * To get a focus manager, call the `useFocusManager` hook from a component within the FocusScope. A focus manager supports the following methods: | Method | Description | | --- | --- | | `focusNext( (opts?: FocusManagerOptions )): FocusableElement | null` | Moves focus to the next focusable or tabbable element in the focus scope. | | `focusPrevious( (opts?: FocusManagerOptions )): FocusableElement | null` | Moves focus to the previous focusable or tabbable element in the focus scope. | | `focusFirst( (opts?: FocusManagerOptions )): FocusableElement | null` | Moves focus to the first focusable or tabbable element in the focus scope. | | `focusLast( (opts?: FocusManagerOptions )): FocusableElement | null` | Moves focus to the last focusable or tabbable element in the focus scope. | ## Example# * * * A basic example of a focus scope that contains focus within it is below. Clicking the "Open" button mounts a FocusScope, which auto focuses the first input inside it. Once open, you can press the Tab key to move within the scope, but focus is contained inside. Clicking the "Close" button unmounts the focus scope, which restores focus back to the button. For a full example of building a modal dialog, see useDialog. import {FocusScope} from 'react-aria'; function Example() { let [isOpen, setOpen] = React.useState(false); return ( <> <button onClick={() => setOpen(true)}>Open</button> {isOpen && ( <FocusScope contain restoreFocus autoFocus> <label htmlFor="first-input">First Input</label> <input id="first-input" /> <label htmlFor="second-input">Second Input</label> <input id="second-input" /> <button onClick={() => setOpen(false)}>Close</button> </FocusScope> )} </> ); } import {FocusScope} from 'react-aria'; function Example() { let [isOpen, setOpen] = React.useState(false); return ( <> <button onClick={() => setOpen(true)}>Open</button> {isOpen && ( <FocusScope contain restoreFocus autoFocus> <label htmlFor="first-input">First Input</label> <input id="first-input" /> <label htmlFor="second-input"> Second Input </label> <input id="second-input" /> <button onClick={() => setOpen(false)}> Close </button> </FocusScope> )} </> ); } import {FocusScope} from 'react-aria'; function Example() { let [isOpen, setOpen] = React.useState( false ); return ( <> <button onClick={() => setOpen(true)} > Open </button> {isOpen && ( <FocusScope contain restoreFocus autoFocus > <label htmlFor="first-input"> First Input </label> <input id="first-input" /> <label htmlFor="second-input"> Second Input </label> <input id="second-input" /> <button onClick={() => setOpen( false )} > Close </button> </FocusScope> )} </> ); } Open ## useFocusManager example# * * * This example shows how to use `useFocusManager` to programmatically move focus within a `FocusScope`. It implements a basic toolbar component, which allows using the left and right arrow keys to move focus to the previous and next buttons. The `wrap` option is used to make focus wrap around when it reaches the first or last button. import {useFocusManager} from 'react-aria'; function Toolbar(props) { return ( <div role="toolbar"> <FocusScope> {props.children} </FocusScope> </div> ); } function ToolbarButton(props) { let focusManager = useFocusManager(); let onKeyDown = (e) => { switch (e.key) { case 'ArrowRight': focusManager.focusNext({ wrap: true }); break; case 'ArrowLeft': focusManager.focusPrevious({ wrap: true }); break; } }; return ( <button onKeyDown={onKeyDown} > {props.children} </button> ); } <Toolbar> <ToolbarButton>Cut</ToolbarButton> <ToolbarButton>Copy</ToolbarButton> <ToolbarButton>Paste</ToolbarButton> </Toolbar> import {useFocusManager} from 'react-aria'; function Toolbar(props) { return ( <div role="toolbar"> <FocusScope> {props.children} </FocusScope> </div> ); } function ToolbarButton(props) { let focusManager = useFocusManager(); let onKeyDown = (e) => { switch (e.key) { case 'ArrowRight': focusManager.focusNext({ wrap: true }); break; case 'ArrowLeft': focusManager.focusPrevious({ wrap: true }); break; } }; return ( <button onKeyDown={onKeyDown} > {props.children} </button> ); } <Toolbar> <ToolbarButton>Cut</ToolbarButton> <ToolbarButton>Copy</ToolbarButton> <ToolbarButton>Paste</ToolbarButton> </Toolbar> import {useFocusManager} from 'react-aria'; function Toolbar(props) { return ( <div role="toolbar"> <FocusScope> {props.children} </FocusScope> </div> ); } function ToolbarButton( props ) { let focusManager = useFocusManager(); let onKeyDown = ( e ) => { switch (e.key) { case 'ArrowRight': focusManager .focusNext({ wrap: true }); break; case 'ArrowLeft': focusManager .focusPrevious( { wrap: true } ); break; } }; return ( <button onKeyDown={onKeyDown} > {props.children} </button> ); } <Toolbar> <ToolbarButton> Cut </ToolbarButton> <ToolbarButton> Copy </ToolbarButton> <ToolbarButton> Paste </ToolbarButton> </Toolbar> CutCopyPaste --- ## Page: https://react-spectrum.adobe.com/react-aria/useFocusRing.html # useFocusRing Determines whether a focus ring should be shown to indicate keyboard focus. Focus rings are visible only when the user is interacting with a keyboard, not with a mouse, touch, or other input methods. <table><tbody><tr><th>install</th><td><code>yarn add react-aria</code></td></tr><tr><th>version</th><td>3.41.1</td></tr><tr><th>usage</th><td><code><span>import</span> {useFocusRing} <span>from</span> <span>'react-aria'</span></code></td></tr></tbody></table> View repository GitHub View package NPM ## API# * * * `useFocusRing( (props: AriaFocusRingProps )): FocusRingAria ` ## Introduction# * * * The `useFocusRing` hook returns whether a focus ring should be displayed to indicate keyboard focus for a component. This helps keyboard users determine which element on a page or in an application has keyboard focus as they navigate around. Focus rings are only visible when interacting with a keyboard so as not to distract mouse and touch screen users. If CSS classes are being used for styling, see the FocusRing component for a shortcut. ## Options# * * * | Name | Type | Default | Description | | --- | --- | --- | --- | | `within` | `boolean` | `'false'` | Whether to show the focus ring when something inside the container element has focus (true), or only if the container itself has focus (false). | | `isTextInput` | `boolean` | — | Whether the element is a text input. | | `autoFocus` | `boolean` | — | Whether the element will be auto focused. | ## Result# * * * | Name | Type | Description | | --- | --- | --- | | `isFocused` | `boolean` | Whether the element is currently focused. | | `isFocusVisible` | `boolean` | Whether keyboard focus should be visible. | | `focusProps` | `DOMAttributes` | Props to apply to the container element with the focus ring. | ## Example# * * * This example shows how to use `useFocusRing` to adjust styling when keyboard focus is on a button. Specifically, the `outline` property is used to create the focus ring when `isFocusVisible` is true. See useCheckbox, useRadioGroup, and useSwitch for more advanced examples of focus rings with other styling techniques. import {useFocusRing} from 'react-aria'; function Example() { let { isFocusVisible, focusProps } = useFocusRing(); return ( <button {...focusProps} style={{ WebkitAppearance: 'none', appearance: 'none', background: 'green', border: 'none', color: 'white', fontSize: 14, padding: '4px 8px', outline: isFocusVisible ? '2px solid dodgerblue' : 'none', outlineOffset: 2 }} > Test </button> ); } import {useFocusRing} from 'react-aria'; function Example() { let { isFocusVisible, focusProps } = useFocusRing(); return ( <button {...focusProps} style={{ WebkitAppearance: 'none', appearance: 'none', background: 'green', border: 'none', color: 'white', fontSize: 14, padding: '4px 8px', outline: isFocusVisible ? '2px solid dodgerblue' : 'none', outlineOffset: 2 }} > Test </button> ); } import {useFocusRing} from 'react-aria'; function Example() { let { isFocusVisible, focusProps } = useFocusRing(); return ( <button {...focusProps} style={{ WebkitAppearance: 'none', appearance: 'none', background: 'green', border: 'none', color: 'white', fontSize: 14, padding: '4px 8px', outline: isFocusVisible ? '2px solid dodgerblue' : 'none', outlineOffset: 2 }} > Test </button> ); } Test --- ## Page: https://react-spectrum.adobe.com/react-aria/I18nProvider.html # I18nProvider Provides the locale for the application to all child components. <table><tbody><tr><th>install</th><td><code>yarn add react-aria</code></td></tr><tr><th>version</th><td>3.41.1</td></tr><tr><th>usage</th><td><code><span>import</span> {I18nProvider} <span>from</span> <span>'react-aria'</span></code></td></tr></tbody></table> View repository GitHub View package NPM ## Introduction# * * * `I18nProvider` allows you to override the default locale as determined by the browser/system setting with a locale defined by your application (e.g. application setting). This should be done by wrapping your entire application in the provider, which will be cause all child elements to receive the new locale information via useLocale. ## Props# * * * | Name | Type | Description | | --- | --- | --- | | `children` | `ReactNode` | Contents that should have the locale applied. | | `locale` | `string` | The locale to apply to the children. | ## Example# * * * import {I18nProvider} from 'react-aria'; <I18nProvider locale="fr-FR"> <YourApp /> </I18nProvider> import {I18nProvider} from 'react-aria'; <I18nProvider locale="fr-FR"> <YourApp /> </I18nProvider> import {I18nProvider} from 'react-aria'; <I18nProvider locale="fr-FR"> <YourApp /> </I18nProvider> --- ## Page: https://react-spectrum.adobe.com/react-aria/useCollator.html # useCollator Provides localized string collation for the current locale. Automatically updates when the locale changes, and handles caching of the collator for performance. <table><tbody><tr><th>install</th><td><code>yarn add react-aria</code></td></tr><tr><th>version</th><td>3.41.1</td></tr><tr><th>usage</th><td><code><span>import</span> {useCollator} <span>from</span> <span>'react-aria'</span></code></td></tr></tbody></table> View repository GitHub View package NPM ## Introduction# * * * `useCollator` wraps a builtin browser Intl.Collator object to provide a React Hook that integrates with the i18n system in React Aria. It handles string comparison according to the current locale, updating when the locale changes, and caching of collators for performance. See the Intl.Collator docs for information. ## API# * * * `useCollator( (options?: Intl.CollatorOptions )): Intl.Collator` ## Example# * * * This example includes two textfields and compares the values of the two fields using a collator according to the current locale. import {useCollator} from 'react-aria'; function Example() { let [first, setFirst] = React.useState(''); let [second, setSecond] = React.useState(''); let collator = useCollator(); let result = collator.compare(first, second); return ( <> <div> <label htmlFor="first-string">First string</label> <input id="first-string" value={first} onChange={(e) => setFirst(e.target.value)} /> <label htmlFor="second-string">Second string</label> <input id="second-string" value={second} onChange={(e) => setSecond(e.target.value)} /> </div> <p> {result === 0 ? 'The strings are the same' : result < 0 ? 'First comes before second' : 'Second comes before first'} </p> </> ); } import {useCollator} from 'react-aria'; function Example() { let [first, setFirst] = React.useState(''); let [second, setSecond] = React.useState(''); let collator = useCollator(); let result = collator.compare(first, second); return ( <> <div> <label htmlFor="first-string">First string</label> <input id="first-string" value={first} onChange={(e) => setFirst(e.target.value)} /> <label htmlFor="second-string">Second string</label> <input id="second-string" value={second} onChange={(e) => setSecond(e.target.value)} /> </div> <p> {result === 0 ? 'The strings are the same' : result < 0 ? 'First comes before second' : 'Second comes before first'} </p> </> ); } import {useCollator} from 'react-aria'; function Example() { let [first, setFirst] = React.useState(''); let [ second, setSecond ] = React.useState(''); let collator = useCollator(); let result = collator .compare( first, second ); return ( <> <div> <label htmlFor="first-string"> First string </label> <input id="first-string" value={first} onChange={(e) => setFirst( e.target .value )} /> <label htmlFor="second-string"> Second string </label> <input id="second-string" value={second} onChange={(e) => setSecond( e.target .value )} /> </div> <p> {result === 0 ? 'The strings are the same' : result < 0 ? 'First comes before second' : 'Second comes before first'} </p> </> ); } First stringSecond string The strings are the same --- ## Page: https://react-spectrum.adobe.com/react-aria/useDateFormatter.html # useDateFormatter Provides localized date formatting for the current locale. Automatically updates when the locale changes, and handles caching of the date formatter for performance. <table><tbody><tr><th>install</th><td><code>yarn add react-aria</code></td></tr><tr><th>version</th><td>3.41.1</td></tr><tr><th>usage</th><td><code><span>import</span> {useDateFormatter} <span>from</span> <span>'react-aria'</span></code></td></tr></tbody></table> View repository GitHub View package NPM ## Introduction# * * * `useDateFormatter` wraps a builtin browser Intl.DateTimeFormat object to provide a React Hook that integrates with the i18n system in React Aria. It handles formatting dates for the current locale, updating when the locale changes, and caching of date formatters for performance. See the Intl.DateTimeFormat docs for information on formatting options. ## API# * * * `useDateFormatter( (options?: DateFormatterOptions )): DateFormatter ` ## Example# * * * This example displays the current date for two locales: USA, and Russia. Two instances of the `CurrentDate` component are rendered, using the I18nProvider to specify the locale to display. import {I18nProvider, useDateFormatter} from 'react-aria'; function CurrentDate() { let formatter = useDateFormatter(); return <p>{formatter.format(new Date())}</p>; } <> <I18nProvider locale="en-US"> <CurrentDate /> </I18nProvider> <I18nProvider locale="ru-RU"> <CurrentDate /> </I18nProvider> </> import {I18nProvider, useDateFormatter} from 'react-aria'; function CurrentDate() { let formatter = useDateFormatter(); return <p>{formatter.format(new Date())}</p>; } <> <I18nProvider locale="en-US"> <CurrentDate /> </I18nProvider> <I18nProvider locale="ru-RU"> <CurrentDate /> </I18nProvider> </> import { I18nProvider, useDateFormatter } from 'react-aria'; function CurrentDate() { let formatter = useDateFormatter(); return ( <p> {formatter.format( new Date() )} </p> ); } <> <I18nProvider locale="en-US"> <CurrentDate /> </I18nProvider> <I18nProvider locale="ru-RU"> <CurrentDate /> </I18nProvider> </> 6/18/2025 18.06.2025 --- ## Page: https://react-spectrum.adobe.com/react-aria/useFilter.html # useFilter Provides localized string search functionality that is useful for filtering or matching items in a list. Options can be provided to adjust the sensitivity to case, diacritics, and other parameters. <table><tbody><tr><th>install</th><td><code>yarn add react-aria</code></td></tr><tr><th>version</th><td>3.41.1</td></tr><tr><th>usage</th><td><code><span>import</span> {useFilter} <span>from</span> <span>'react-aria'</span></code></td></tr></tbody></table> View repository GitHub View package NPM ## Introduction# * * * `useFilter` provides functions for filtering or searching based on substring matches. The builtin JavaScript string methods `startsWith`, `endsWith`, and `includes` could be used for this, but do not implement locale sensitive matching. `useFilter` provides options to allow ignoring case, diacritics, and Unicode normalization forms, which are implemented according to locale-specific rules. It automatically uses the current locale set by the application, either via the default browser language or via the I18nProvider. ## API# * * * `useFilter( (options?: Intl.CollatorOptions )): Filter ` ## Interface# * * * | Method | Description | | --- | --- | | `startsWith( (string: string, , substring: string )): boolean` | Returns whether a string starts with a given substring. | | `endsWith( (string: string, , substring: string )): boolean` | Returns whether a string ends with a given substring. | | `contains( (string: string, , substring: string )): boolean` | Returns whether a string contains a given substring. | ## Example# * * * The following example implements a filterable list using a `contains` matching strategy that ignores both case and diacritics. import {useFilter} from 'react-aria'; function Example() { const composers = [ 'Wolfgang Amadeus Mozart', 'Johann Sebastian Bach', 'Ludwig van Beethoven', 'Claude Debussy', 'George Frideric Handel', 'Frédéric Chopin', 'Johannes Brahms', 'Pyotr Ilyich Tchaikovsky', 'Antonín Dvořák', 'Felix Mendelssohn', 'Béla Bartók', 'Niccolò Paganini' ]; let { contains } = useFilter({ sensitivity: 'base' }); let [value, setValue] = React.useState(''); let matchedComposers = composers.filter((composer) => contains(composer, value) ); return ( <> <label htmlFor="search-input">Filter:</label> <input type="search" id="search-input" value={value} onChange={(e) => setValue(e.target.value)} /> <ul style={{ height: 300 }}> {matchedComposers.map((composer, i) => <li key={i}>{composer}</li>)} </ul> </> ); } import {useFilter} from 'react-aria'; function Example() { const composers = [ 'Wolfgang Amadeus Mozart', 'Johann Sebastian Bach', 'Ludwig van Beethoven', 'Claude Debussy', 'George Frideric Handel', 'Frédéric Chopin', 'Johannes Brahms', 'Pyotr Ilyich Tchaikovsky', 'Antonín Dvořák', 'Felix Mendelssohn', 'Béla Bartók', 'Niccolò Paganini' ]; let { contains } = useFilter({ sensitivity: 'base' }); let [value, setValue] = React.useState(''); let matchedComposers = composers.filter((composer) => contains(composer, value) ); return ( <> <label htmlFor="search-input">Filter:</label> <input type="search" id="search-input" value={value} onChange={(e) => setValue(e.target.value)} /> <ul style={{ height: 300 }}> {matchedComposers.map((composer, i) => ( <li key={i}>{composer}</li> ))} </ul> </> ); } import {useFilter} from 'react-aria'; function Example() { const composers = [ 'Wolfgang Amadeus Mozart', 'Johann Sebastian Bach', 'Ludwig van Beethoven', 'Claude Debussy', 'George Frideric Handel', 'Frédéric Chopin', 'Johannes Brahms', 'Pyotr Ilyich Tchaikovsky', 'Antonín Dvořák', 'Felix Mendelssohn', 'Béla Bartók', 'Niccolò Paganini' ]; let { contains } = useFilter({ sensitivity: 'base' }); let [value, setValue] = React.useState(''); let matchedComposers = composers.filter( (composer) => contains( composer, value ) ); return ( <> <label htmlFor="search-input"> Filter: </label> <input type="search" id="search-input" value={value} onChange={(e) => setValue( e.target .value )} /> <ul style={{ height: 300 }} > {matchedComposers .map(( composer, i ) => ( <li key={i}> {composer} </li> ))} </ul> </> ); } Filter: * Wolfgang Amadeus Mozart * Johann Sebastian Bach * Ludwig van Beethoven * Claude Debussy * George Frideric Handel * Frédéric Chopin * Johannes Brahms * Pyotr Ilyich Tchaikovsky * Antonín Dvořák * Felix Mendelssohn * Béla Bartók * Niccolò Paganini --- ## Page: https://react-spectrum.adobe.com/react-aria/useLocale.html # useLocale Returns the current locale and layout direction. <table><tbody><tr><th>install</th><td><code>yarn add react-aria</code></td></tr><tr><th>version</th><td>3.41.1</td></tr><tr><th>usage</th><td><code><span>import</span> {useLocale} <span>from</span> <span>'react-aria'</span></code></td></tr></tbody></table> View repository GitHub View package NPM ## Introduction# * * * `useLocale` allows components to access the current locale and interface layout direction. By default, this is automatically detected based on the browser or system language, but it can be overridden by using the I18nProvider at the root of your app. `useLocale` should be used in the root of your app to define the lang and dir attributes so that the browser knows which language and direction the user interface should be rendered in. ## API# * * * `useLocale(): Locale ` ## Interface# * * * | Name | Type | Description | | --- | --- | --- | | `locale` | `string` | The BCP47 language code for the locale. | | `direction` | ` Direction ` | The writing direction for the locale. | ## Example# * * * import {useLocale} from 'react-aria'; function YourApp() { let { locale, direction } = useLocale(); return ( <div lang={locale} dir={direction}> {/* your app here */} </div> ); } import {useLocale} from 'react-aria'; function YourApp() { let { locale, direction } = useLocale(); return ( <div lang={locale} dir={direction}> {/* your app here */} </div> ); } import {useLocale} from 'react-aria'; function YourApp() { let { locale, direction } = useLocale(); return ( <div lang={locale} dir={direction} > {/* your app here */} </div> ); } --- ## Page: https://react-spectrum.adobe.com/react-aria/useNumberFormatter.html # useNumberFormatter Provides localized number formatting for the current locale. Automatically updates when the locale changes, and handles caching of the number formatter for performance. <table><tbody><tr><th>install</th><td><code>yarn add react-aria</code></td></tr><tr><th>version</th><td>3.41.1</td></tr><tr><th>usage</th><td><code><span>import</span> {useNumberFormatter} <span>from</span> <span>'react-aria'</span></code></td></tr></tbody></table> View repository GitHub View package NPM ## Introduction# * * * `useNumberFormatter` wraps a builtin browser Intl.NumberFormat object to provide a React Hook that integrates with the i18n system in React Aria. It handles formatting numbers for the current locale, updating when the locale changes, and caching of number formatters for performance. See the Intl.NumberFormat docs for information on formatting options. ## API# * * * `useNumberFormatter( (options: NumberFormatOptions )): Intl.NumberFormat` ## Example# * * * This example displays a currency value for two locales: USA, and Germany. Two instances of the `Currency` component are rendered, using the I18nProvider to specify the locale to display. import {I18nProvider, useNumberFormatter} from 'react-aria'; function Currency({ value, currency }) { let formatter = useNumberFormatter({ style: 'currency', currency, minimumFractionDigits: 0 }); return <p>{formatter.format(value)}</p>; } <> <I18nProvider locale="en-US"> <Currency value={125000} currency="USD" /> </I18nProvider> <I18nProvider locale="de-DE"> <Currency value={350000} currency="EUR" /> </I18nProvider> </> import {I18nProvider, useNumberFormatter} from 'react-aria'; function Currency({ value, currency }) { let formatter = useNumberFormatter({ style: 'currency', currency, minimumFractionDigits: 0 }); return <p>{formatter.format(value)}</p>; } <> <I18nProvider locale="en-US"> <Currency value={125000} currency="USD" /> </I18nProvider> <I18nProvider locale="de-DE"> <Currency value={350000} currency="EUR" /> </I18nProvider> </> import { I18nProvider, useNumberFormatter } from 'react-aria'; function Currency( { value, currency } ) { let formatter = useNumberFormatter({ style: 'currency', currency, minimumFractionDigits: 0 }); return ( <p> {formatter.format( value )} </p> ); } <> <I18nProvider locale="en-US"> <Currency value={125000} currency="USD" /> </I18nProvider> <I18nProvider locale="de-DE"> <Currency value={350000} currency="EUR" /> </I18nProvider> </> $125,000 350.000 € --- ## Page: https://react-spectrum.adobe.com/react-aria/SSRProvider.html # SSRProvider When using SSR with React Aria in React 16 or 17, applications must be wrapped in an SSRProvider. This ensures that auto generated ids are consistent between the client and server. <table><tbody><tr><th>install</th><td><code>yarn add react-aria</code></td></tr><tr><th>version</th><td>3.41.1</td></tr><tr><th>usage</th><td><code><span>import</span> {SSRProvider} <span>from</span> <span>'react-aria'</span></code></td></tr></tbody></table> View repository GitHub View package NPM ## Introduction# * * * If you're using React 16 or 17, `SSRProvider` should be used as a wrapper for the entire application during server side rendering. It works together with the useId hook to ensure that auto generated ids are consistent between the client and server by resetting the id internal counter on each request. See the server side rendering docs for more information. **Note**: if you're using React 18 or newer, `SSRProvider` is not necessary and can be removed from your app. React Aria uses the React.useId hook internally when using React 18, which ensures ids are consistent. ## Props# * * * | Name | Type | Description | | --- | --- | --- | | `children` | `ReactNode` | Your application here. | ## Example# * * * import {SSRProvider} from 'react-aria'; <SSRProvider> <YourApp /> </SSRProvider> import {SSRProvider} from 'react-aria'; <SSRProvider> <YourApp /> </SSRProvider> import {SSRProvider} from 'react-aria'; <SSRProvider> <YourApp /> </SSRProvider> --- ## Page: https://react-spectrum.adobe.com/react-aria/useIsSSR.html # useIsSSR Returns whether the component is currently being server side rendered or hydrated on the client. Can be used to delay browser-specific rendering until after hydration. <table><tbody><tr><th>install</th><td><code>yarn add react-aria</code></td></tr><tr><th>version</th><td>3.41.1</td></tr><tr><th>usage</th><td><code><span>import</span> {useIsSSR} <span>from</span> <span>'react-aria'</span></code></td></tr></tbody></table> View repository GitHub View package NPM ## API# * * * `useIsSSR(): boolean` ## Introduction# * * * `useIsSSR` returns `true` during server rendering and hydration, and updates to `false` immediately after hydration. This can be used to ensure that the server rendered HTML and initially hydrated DOM match, but trigger an additional render after hydration to run browser-specific code. For example, it could be used to run media queries or feature detection for browser-specific APIs that affect rendering but cannot be run server side. In React 16 and 17, this hook must be used in combination with the SSRProvider component wrapping your application. See the server side rendering docs for more information. ## Example# * * * import {useIsSSR} from 'react-aria'; function MyComponent() { let isSSR = useIsSSR(); return <span>{isSSR ? 'Server' : 'Client'}</span>; } import {useIsSSR} from 'react-aria'; function MyComponent() { let isSSR = useIsSSR(); return <span>{isSSR ? 'Server' : 'Client'}</span>; } import {useIsSSR} from 'react-aria'; function MyComponent() { let isSSR = useIsSSR(); return ( <span> {isSSR ? 'Server' : 'Client'} </span> ); } --- ## Page: https://react-spectrum.adobe.com/react-aria/PortalProvider.html # PortalProvider Sets the portal container for all overlay elements rendered by its children. <table><tbody><tr><th>install</th><td><code>yarn add react-aria</code></td></tr><tr><th>version</th><td>3.41.1</td></tr><tr><th>usage</th><td><code><span>import</span> {UNSAFE_PortalProvider, useUNSAFE_PortalContext} <span>from</span> <span>'react-aria'</span></code></td></tr></tbody></table> View repository GitHub View package NPM ## Introduction# * * * `UNSAFE_PortalProvider` is a utility wrapper component that can be used to set where components like Modals, Popovers, Toasts, and Tooltips will portal their overlay element to. This is typically used when your app is already portalling other elements to a location other than the `document.body` and thus requires your React Aria components to send their overlays to the same container. Please note that `UNSAFE_PortalProvider` is considered `UNSAFE` because it is an escape hatch, and there are many places that an application could portal to. Not all of them will work, either with styling, accessibility, or for a variety of other reasons. Typically, it is best to portal to the root of the entire application, e.g. the `body` element, outside of any possible overflow or stacking contexts. We envision `UNSAFE_PortalProvider` being used to group all of the portalled elements into a single container at the root of the app or to control the order of children of the `body` element, but you may have use cases that need to do otherwise. ## Props# * * * | Name | Type | Description | | --- | --- | --- | | `children` | `ReactNode` | The content of the PortalProvider. Should contain all children that want to portal their overlays to the element returned by the provided `getContainer()`. | | `getContainer` | `() => HTMLElement | null | null` | Should return the element where we should portal to. Can clear the context by passing null. | ## Example# * * * The example below shows how you can use `UNSAFE_PortalProvider` to portal your Toasts to an arbitrary container. Note that the Toast in this example is taken directly from the React Aria Components Toast documentation, please visit that page for a detailed explanation of its implementation. import {UNSAFE_PortalProvider} from 'react-aria'; // See the above Toast docs link for the ToastRegion implementation function App() { let container = React.useRef(null); return ( <> <UNSAFE_PortalProvider getContainer={() => container.current}> <MyToastRegion /> <Button onPress={() => queue.add({ title: 'Toast complete!', description: 'Great success.' })} > Open Toast </Button> </UNSAFE_PortalProvider> <div ref={container} style={{ height: '110px', width: '200px', overflow: 'auto', display: 'flex', flexDirection: 'column', gap: '20px', padding: '5px' }} > Toasts are portalled here! </div> </> ); } <App /> import {UNSAFE_PortalProvider} from 'react-aria'; // See the above Toast docs link for the ToastRegion implementation function App() { let container = React.useRef(null); return ( <> <UNSAFE_PortalProvider getContainer={() => container.current} > <MyToastRegion /> <Button onPress={() => queue.add({ title: 'Toast complete!', description: 'Great success.' })} > Open Toast </Button> </UNSAFE_PortalProvider> <div ref={container} style={{ height: '110px', width: '200px', overflow: 'auto', display: 'flex', flexDirection: 'column', gap: '20px', padding: '5px' }} > Toasts are portalled here! </div> </> ); } <App /> import {UNSAFE_PortalProvider} from 'react-aria'; // See the above Toast docs link for the ToastRegion implementation function App() { let container = React .useRef(null); return ( <> <UNSAFE_PortalProvider getContainer={() => container .current} > <MyToastRegion /> <Button onPress={() => queue.add({ title: 'Toast complete!', description: 'Great success.' })} > Open Toast </Button> </UNSAFE_PortalProvider> <div ref={container} style={{ height: '110px', width: '200px', overflow: 'auto', display: 'flex', flexDirection: 'column', gap: '20px', padding: '5px' }} > Toasts are portalled here! </div> </> ); } <App /> Open Toast Toasts are portalled here! ## Contexts# * * * The `getContainer` set by the nearest PortalProvider can be accessed by calling `useUNSAFE_PortalContext`. This can be used by custom overlay components to ensure that they are also being consistently portalled throughout your app. `useUNSAFE_PortalContext(): PortalProviderContextValue ` import {useUNSAFE_PortalContext} from 'react-aria'; function MyOverlay(props) { let { children } = props; let { getContainer } = useUNSAFE_PortalContext(); return ReactDOM.createPortal(children, getContainer()); } import {useUNSAFE_PortalContext} from 'react-aria'; function MyOverlay(props) { let { children } = props; let { getContainer } = useUNSAFE_PortalContext(); return ReactDOM.createPortal(children, getContainer()); } import {useUNSAFE_PortalContext} from 'react-aria'; function MyOverlay( props ) { let { children } = props; let { getContainer } = useUNSAFE_PortalContext(); return ReactDOM .createPortal( children, getContainer() ); } --- ## Page: https://react-spectrum.adobe.com/react-aria/VisuallyHidden.html # VisuallyHidden VisuallyHidden hides its children visually, while keeping content visible to screen readers. <table><tbody><tr><th>install</th><td><code>yarn add react-aria</code></td></tr><tr><th>version</th><td>3.41.1</td></tr><tr><th>usage</th><td><code><span>import</span> {VisuallyHidden} <span>from</span> <span>'react-aria'</span></code></td></tr></tbody></table> View repository GitHub View package NPM ## Introduction# * * * `VisuallyHidden` is a utility component that can be used to hide its children visually, while keeping them visible to screen readers and other assistive technology. This would typically be used when you want to take advantage of the behavior and semantics of a native element like a checkbox or radio button, but replace it with a custom styled element visually. ## Props# * * * | Name | Type | Default | Description | | --- | --- | --- | --- | | `children` | `ReactNode` | — | The content to visually hide. | | `elementType` | `string | JSXElementConstructor<any>` | `'div'` | The element type for the container. | | `isFocusable` | `boolean` | — | Whether the element should become visible on focus, for example skip links. | | `id` | `string | undefined` | — | | | `style` | `CSSProperties | undefined` | — | | | `className` | `string | undefined` | — | | Accessibility | Name | Type | Description | | --- | --- | --- | | `role` | `AriaRole | undefined` | | | `tabIndex` | `number | undefined` | | ## Example# * * * See useRadioGroup and useCheckbox for examples of using `VisuallyHidden` to hide native form elements visually. ## Hook# * * * In order to allow additional rendering flexibility, the `useVisuallyHidden` hook can be used in custom components instead of the `VisuallyHidden` component. It supports the same options as the component, and returns props to spread onto the element that should be hidden. `useVisuallyHidden( (props: VisuallyHiddenProps )): VisuallyHiddenAria ` import {useVisuallyHidden} from 'react-aria'; let { visuallyHiddenProps } = useVisuallyHidden(); <div {...visuallyHiddenProps}>I am hidden</div> import {useVisuallyHidden} from 'react-aria'; let { visuallyHiddenProps } = useVisuallyHidden(); <div {...visuallyHiddenProps}>I am hidden</div> import {useVisuallyHidden} from 'react-aria'; let { visuallyHiddenProps } = useVisuallyHidden(); <div {...visuallyHiddenProps} > I am hidden </div> ## Gotchas# * * * VisuallyHidden is positioned absolutely, so it needs a positioned ancestor. Otherwise, it will be positioned relative to the nearest positioned ancestor, which may be the body, causing undesired scroll bars to appear. <label style={{position: 'relative'}}> <VisuallyHidden> <input type="checkbox" /> </VisuallyHidden> <span>Subscribe to our newsletter</span> </label> <label style={{position: 'relative'}}> <VisuallyHidden> <input type="checkbox" /> </VisuallyHidden> <span>Subscribe to our newsletter</span> </label> <label style={{ position: 'relative' }} > <VisuallyHidden> <input type="checkbox" /> </VisuallyHidden> <span> Subscribe to our newsletter </span> </label> --- ## Page: https://react-spectrum.adobe.com/react-aria/mergeProps.html # mergeProps Merges multiple props objects together. Event handlers are chained, classNames are combined, and ids are deduplicated - different ids will trigger a side-effect and re-render components hooked up with \`useId\`. For all other props, the last prop object overrides all previous ones. <table><tbody><tr><th>install</th><td><code>yarn add react-aria</code></td></tr><tr><th>version</th><td>3.41.1</td></tr><tr><th>usage</th><td><code><span>import</span> {mergeProps} <span>from</span> <span>'react-aria'</span></code></td></tr></tbody></table> View repository GitHub View package NPM ## API# * * * `mergeProps<T extends PropsArg []>( (...args: T )): UnionToIntersection < TupleTypes <T>>` ## Introduction# * * * `mergeProps` is a utility function that combines multiple props objects together in a smart way. This can be useful when you need to combine the results of multiple react-aria hooks onto a single element. For example, both hooks may return the same event handlers, class names, or ids, and only one of these can be applied. `mergeProps` handles combining these props together so that multiple event handlers will be chained, multiple classes will be combined, and ids will be deduplicated. For all other props, the last prop object overrides all previous ones. ## Example# * * * import {mergeProps} from 'react-aria'; let a = { className: 'foo', onKeyDown(e) { if (e.key === 'Enter') { console.log('enter'); } } }; let b = { className: 'bar', onKeyDown(e) { if (e.key === ' ') { console.log('space'); } } }; let merged = mergeProps(a, b); import {mergeProps} from 'react-aria'; let a = { className: 'foo', onKeyDown(e) { if (e.key === 'Enter') { console.log('enter'); } } }; let b = { className: 'bar', onKeyDown(e) { if (e.key === ' ') { console.log('space'); } } }; let merged = mergeProps(a, b); import {mergeProps} from 'react-aria'; let a = { className: 'foo', onKeyDown(e) { if ( e.key === 'Enter' ) { console.log( 'enter' ); } } }; let b = { className: 'bar', onKeyDown(e) { if (e.key === ' ') { console.log( 'space' ); } } }; let merged = mergeProps( a, b ); The result of the above example will be equivalent to this: let merged = { className: 'foo bar', onKeyDown(e) { a.onKeyDown(e); b.onKeyDown(e); } }; let merged = { className: 'foo bar', onKeyDown(e) { a.onKeyDown(e); b.onKeyDown(e); } }; let merged = { className: 'foo bar', onKeyDown(e) { a.onKeyDown(e); b.onKeyDown(e); } }; --- ## Page: https://react-spectrum.adobe.com/react-aria/useField.html # useField Provides the accessibility implementation for input fields. Fields accept user input, gain context from their label, and may display a description or error message. <table><tbody><tr><th>install</th><td><code>yarn add react-aria</code></td></tr><tr><th>version</th><td>3.41.1</td></tr><tr><th>usage</th><td><code><span>import</span> {useField} <span>from</span> <span>'react-aria'</span></code></td></tr></tbody></table> View repository GitHub View package NPM ## API# * * * `useField( (props: AriaFieldProps )): FieldAria ` ## Example# * * * The `useField` hook associates a form control with a label, and an optional description and/or error message. This is useful for providing context about how users should fill in a field, or a validation message. `useField` takes care of creating ids for each element and associating them with the correct ARIA attributes (`aria-labelledby` and `aria-describedby`). By default, `useField` assumes that the label is a native HTML `<label>` element. However, if you are labeling a non-native form element, be sure to use an element other than a `<label>` and set the `labelElementType` prop appropriately. **Note**: Many other React Aria hooks such as useTextField, useSelect, and useComboBox already include support for description and error message elements. If you're using one of those hooks, there's no need to use `useField`. import {useField} from 'react-aria'; function ContactPicker(props) { let { labelProps, fieldProps, descriptionProps, errorMessageProps } = useField(props); return ( <div style={{ display: 'flex', flexDirection: 'column', width: 200, marginBottom: 20 }} > <label {...labelProps}>{props.label}</label> <select {...fieldProps}> <option>Email</option> <option>Phone</option> <option>Fax</option> <option>Carrier pigeon</option> </select> {props.description && ( <div {...descriptionProps} style={{ fontSize: 12 }}> {props.description} </div> )} {props.errorMessage && ( <div {...errorMessageProps} style={{ color: 'red', fontSize: 12 }}> {props.errorMessage} </div> )} </div> ); } <ContactPicker label="Preferred contact method" description="Select the best way to contact you about issues with your account." /> <ContactPicker label="Preferred contact method" errorMessage="Select a contact method." /> import {useField} from 'react-aria'; function ContactPicker(props) { let { labelProps, fieldProps, descriptionProps, errorMessageProps } = useField(props); return ( <div style={{ display: 'flex', flexDirection: 'column', width: 200, marginBottom: 20 }} > <label {...labelProps}>{props.label}</label> <select {...fieldProps}> <option>Email</option> <option>Phone</option> <option>Fax</option> <option>Carrier pigeon</option> </select> {props.description && ( <div {...descriptionProps} style={{ fontSize: 12 }} > {props.description} </div> )} {props.errorMessage && ( <div {...errorMessageProps} style={{ color: 'red', fontSize: 12 }} > {props.errorMessage} </div> )} </div> ); } <ContactPicker label="Preferred contact method" description="Select the best way to contact you about issues with your account." /> <ContactPicker label="Preferred contact method" errorMessage="Select a contact method." /> import {useField} from 'react-aria'; function ContactPicker( props ) { let { labelProps, fieldProps, descriptionProps, errorMessageProps } = useField(props); return ( <div style={{ display: 'flex', flexDirection: 'column', width: 200, marginBottom: 20 }} > <label {...labelProps} > {props.label} </label> <select {...fieldProps} > <option> Email </option> <option> Phone </option> <option> Fax </option> <option> Carrier pigeon </option> </select> {props .description && ( <div {...descriptionProps} style={{ fontSize: 12 }} > {props .description} </div> )} {props .errorMessage && ( <div {...errorMessageProps} style={{ color: 'red', fontSize: 12 }} > {props .errorMessage} </div> )} </div> ); } <ContactPicker label="Preferred contact method" description="Select the best way to contact you about issues with your account." /> <ContactPicker label="Preferred contact method" errorMessage="Select a contact method." /> Preferred contact methodEmailPhoneFaxCarrier pigeon Select the best way to contact you about issues with your account. Preferred contact methodEmailPhoneFaxCarrier pigeon Select a contact method. --- ## Page: https://react-spectrum.adobe.com/react-aria/useId.html # useId If a default is not provided, generate an id. <table><tbody><tr><th>install</th><td><code>yarn add react-aria</code></td></tr><tr><th>version</th><td>3.41.1</td></tr><tr><th>usage</th><td><code><span>import</span> {useId} <span>from</span> <span>'react-aria'</span></code></td></tr></tbody></table> View repository GitHub View package NPM ## API# * * * `useId( (defaultId?: string )): string` ## Introduction# * * * The `useId` hook creates an autogenerated unique `id` for an element. An `id` from props can used instead of the autogenerated `id` when available. ## Example# * * * import {useId} from 'react-aria'; let elementId = useId(); let componentId = useId(props.id); import {useId} from 'react-aria'; let elementId = useId(); let componentId = useId(props.id); import {useId} from 'react-aria'; let elementId = useId(); let componentId = useId( props.id ); --- ## Page: https://react-spectrum.adobe.com/react-aria/useLabel.html # useLabel Provides the accessibility implementation for labels and their associated elements. Labels provide context for user inputs. <table><tbody><tr><th>install</th><td><code>yarn add react-aria</code></td></tr><tr><th>version</th><td>3.41.1</td></tr><tr><th>usage</th><td><code><span>import</span> {useLabel} <span>from</span> <span>'react-aria'</span></code></td></tr></tbody></table> View repository GitHub View package NPM ## API# * * * `useLabel( (props: LabelAriaProps )): LabelAria ` ## Example# * * * The `useLabel` hook associates a label with a field. It automatically handles creating an id for the field and associates the label with it. import {useLabel} from 'react-aria'; function ColorField(props) { let { labelProps, fieldProps } = useLabel(props); return ( <> <label {...labelProps}>{props.label}</label> <select {...fieldProps}> <option>Indigo</option> <option>Maroon</option> <option>Chartreuse</option> </select> </> ); } <ColorField label="Favorite color" /> import {useLabel} from 'react-aria'; function ColorField(props) { let { labelProps, fieldProps } = useLabel(props); return ( <> <label {...labelProps}>{props.label}</label> <select {...fieldProps}> <option>Indigo</option> <option>Maroon</option> <option>Chartreuse</option> </select> </> ); } <ColorField label="Favorite color" /> import {useLabel} from 'react-aria'; function ColorField( props ) { let { labelProps, fieldProps } = useLabel(props); return ( <> <label {...labelProps} > {props.label} </label> <select {...fieldProps} > <option> Indigo </option> <option> Maroon </option> <option> Chartreuse </option> </select> </> ); } <ColorField label="Favorite color" /> Favorite colorIndigoMaroonChartreuse By default, `useLabel` assumes that the label is a native HTML label element. However, if you are labeling a non-native form element, be sure to use an element other than a `<label>` and set the `labelElementType` prop appropriately. See useRadioGroup and useTextField for examples of how `useLabel` is used by components. --- ## Page: https://react-spectrum.adobe.com/react-aria/useObjectRef.html # useObjectRef Offers an object ref for a given callback ref or an object ref. Especially helfpul when passing forwarded refs (created using \`React.forwardRef\`) to React Aria hooks. <table><tbody><tr><th>install</th><td><code>yarn add react-aria</code></td></tr><tr><th>version</th><td>3.41.1</td></tr><tr><th>usage</th><td><code><span>import</span> {useObjectRef} <span>from</span> <span>'react-aria'</span></code></td></tr></tbody></table> View repository GitHub View package NPM ## API# * * * `useObjectRef<T>( (ref?: ( (instance: T | | null )) => () => void | | void | MutableRefObject<T | | null> | null )): MutableRefObject<T | null>` ## Introduction# * * * `useObjectRef` is a utility function that will give an object ref back regardless of if a callback ref or object ref was passed in. This is useful for passing refs to React Aria hooks. ## Example# * * * import {useButton, useObjectRef} from 'react-aria'; import {AriaButtonProps} from '@react-types/button'; let Button = React.forwardRef( (props: AriaButtonProps, ref: React.ForwardedRef<HTMLButtonElement>) => { let objRef = useObjectRef(ref); let { buttonProps } = useButton(props, objRef); let { children } = props; return ( <button {...buttonProps} ref={objRef}> {children} </button> ); } ); function MyButton(props) { let ref = React.useRef(null); return ( <Button ref={ref} onPress={() => console.log(ref.current)}> {props.children} </Button> ); } <MyButton>Test</MyButton> import {useButton, useObjectRef} from 'react-aria'; import {AriaButtonProps} from '@react-types/button'; let Button = React.forwardRef( ( props: AriaButtonProps, ref: React.ForwardedRef<HTMLButtonElement> ) => { let objRef = useObjectRef(ref); let { buttonProps } = useButton(props, objRef); let { children } = props; return ( <button {...buttonProps} ref={objRef}> {children} </button> ); } ); function MyButton(props) { let ref = React.useRef(null); return ( <Button ref={ref} onPress={() => console.log(ref.current)} > {props.children} </Button> ); } <MyButton>Test</MyButton> import { useButton, useObjectRef } from 'react-aria'; import {AriaButtonProps} from '@react-types/button'; let Button = React .forwardRef( ( props: AriaButtonProps, ref: React.ForwardedRef< HTMLButtonElement > ) => { let objRef = useObjectRef( ref ); let { buttonProps } = useButton( props, objRef ); let { children } = props; return ( <button {...buttonProps} ref={objRef} > {children} </button> ); } ); function MyButton( props ) { let ref = React.useRef( null ); return ( <Button ref={ref} onPress={() => console.log( ref.current )} > {props.children} </Button> ); } <MyButton> Test </MyButton> Test --- ## Page: https://react-spectrum.adobe.com/react-aria/examples/account-menu.html React AriaExamples # Account Menu A Menu with an interactive header, built with a Dialog and Popover. ## Example# * * * For accessibility reasons, standalone Menus cannot contain any children other than menu items (optionally grouped into sections). To build patterns that include such elements, place the Menu into a Dialog with additional interactive elements as siblings. import {Button, composeRenderProps, Menu, MenuItem, MenuTrigger, Popover, Separator, Switch} from 'react-aria-components'; import type {MenuItemProps, SwitchProps} from 'react-aria-components'; function AccountMenuExample() { return ( <div className="p-8 bg-gray-50 dark:bg-zinc-900 rounded-lg flex items-start justify-center"> <MenuTrigger> <Button aria-label="Account" className="inline-flex items-center justify-center rounded-md p-1.5 text-white bg-transparent border-none hover:bg-gray-200 pressed:bg-gray-300 dark:hover:bg-zinc-800 dark:pressed:bg-zinc-700 transition-colors cursor-default outline-hidden focus-visible:ring-2 focus-visible:ring-blue-600" > <img alt="" src="https://i.imgur.com/xIe7Wlb.png" className="w-7 h-7 rounded-full" /> </Button> <Popover className="p-2 overflow-auto outline-hidden rounded-lg bg-white dark:bg-zinc-950 shadow-lg ring-1 ring-black/10 dark:ring-white/15 entering:animate-in entering:fade-in entering:placement-bottom:slide-in-from-top-1 entering:placement-top:slide-in-from-bottom-1 exiting:animate-out exiting:fade-out exiting:placement-bottom:slide-out-to-top-1 exiting:placement-top:slide-out-to-bottom-1 fill-mode-forwards origin-top-left"> <div className="flex gap-2 items-center mx-3 mt-2"> <img alt="" src="https://i.imgur.com/xIe7Wlb.png" className="w-16 h-16 rounded-full" /> <div className="flex flex-col gap-1"> <div className="text-[15px] font-bold text-gray-900 dark:text-gray-100 leading-none"> Marissa Whitaker </div> <div className="text-base text-gray-900 dark:text-gray-100 leading-none mb-1"> user@example.com </div> <MySwitch>Dark Mode</MySwitch> </div> </div> <Separator className="border-none bg-gray-300 dark:bg-zinc-600 h-[1px] mx-3 mt-4 mb-2" /> <Menu className="outline-hidden"> <MyMenuItem id="new">Account Settings</MyMenuItem> <MyMenuItem id="open">Support</MyMenuItem> <Separator className="bg-gray-300 dark:bg-zinc-600 h-[1px] mx-3 my-2" /> <MyMenuItem id="save">Legal notices</MyMenuItem> <MyMenuItem id="save-as">About</MyMenuItem> <Separator className="bg-gray-300 dark:bg-zinc-600 h-[1px] mx-3 my-2" /> <MyMenuItem id="print">Sign out</MyMenuItem> </Menu> </Popover> </MenuTrigger> </div> ); } function MyMenuItem(props: MenuItemProps) { return ( <MenuItem {...props} className="group flex w-full items-center rounded-md px-3 py-2 box-border outline-hidden cursor-default text-gray-900 dark:text-gray-100 focus:bg-blue-500 focus:text-white" /> ); } function MySwitch(props: SwitchProps) { return ( <Switch className="group relative flex gap-2 items-center text-gray-800 dark:text-zinc-200 text-base transition"> {composeRenderProps(props.children, (children) => ( <> <div className="flex h-3 w-6 p-[2px] items-center shrink-0 cursor-default rounded-full transition duration-200 ease-in-out shadow-inner border border-transparent bg-gray-400 dark:bg-zinc-400 group-pressed:bg-gray-500 dark:group-pressed:bg-zinc-300 group-selected:bg-gray-700 group-selected:dark:bg-zinc-300 group-selected:forced-colors:bg-[Highlight]! group-selected:group-pressed:bg-gray-800 group-selected:dark:group-pressed:bg-zinc-200 outline outline-0 outline-blue-600 dark:outline-blue-500 forced-colors:outline-[Highlight] outline-offset-2 group-focus-visible:outline-2"> <div className="h-3 w-3 transform rounded-full bg-white dark:bg-zinc-900 outline outline-1 -outline-offset-1 outline-transparent shadow-sm transition duration-200 ease-in-out translate-x-0 group-selected:translate-x-[100%]" /> </div> {children} </> ))} </Switch> ); } import { Button, composeRenderProps, Menu, MenuItem, MenuTrigger, Popover, Separator, Switch } from 'react-aria-components'; import type { MenuItemProps, SwitchProps } from 'react-aria-components'; function AccountMenuExample() { return ( <div className="p-8 bg-gray-50 dark:bg-zinc-900 rounded-lg flex items-start justify-center"> <MenuTrigger> <Button aria-label="Account" className="inline-flex items-center justify-center rounded-md p-1.5 text-white bg-transparent border-none hover:bg-gray-200 pressed:bg-gray-300 dark:hover:bg-zinc-800 dark:pressed:bg-zinc-700 transition-colors cursor-default outline-hidden focus-visible:ring-2 focus-visible:ring-blue-600" > <img alt="" src="https://i.imgur.com/xIe7Wlb.png" className="w-7 h-7 rounded-full" /> </Button> <Popover className="p-2 overflow-auto outline-hidden rounded-lg bg-white dark:bg-zinc-950 shadow-lg ring-1 ring-black/10 dark:ring-white/15 entering:animate-in entering:fade-in entering:placement-bottom:slide-in-from-top-1 entering:placement-top:slide-in-from-bottom-1 exiting:animate-out exiting:fade-out exiting:placement-bottom:slide-out-to-top-1 exiting:placement-top:slide-out-to-bottom-1 fill-mode-forwards origin-top-left"> <div className="flex gap-2 items-center mx-3 mt-2"> <img alt="" src="https://i.imgur.com/xIe7Wlb.png" className="w-16 h-16 rounded-full" /> <div className="flex flex-col gap-1"> <div className="text-[15px] font-bold text-gray-900 dark:text-gray-100 leading-none"> Marissa Whitaker </div> <div className="text-base text-gray-900 dark:text-gray-100 leading-none mb-1"> user@example.com </div> <MySwitch>Dark Mode</MySwitch> </div> </div> <Separator className="border-none bg-gray-300 dark:bg-zinc-600 h-[1px] mx-3 mt-4 mb-2" /> <Menu className="outline-hidden"> <MyMenuItem id="new"> Account Settings </MyMenuItem> <MyMenuItem id="open">Support</MyMenuItem> <Separator className="bg-gray-300 dark:bg-zinc-600 h-[1px] mx-3 my-2" /> <MyMenuItem id="save">Legal notices</MyMenuItem> <MyMenuItem id="save-as">About</MyMenuItem> <Separator className="bg-gray-300 dark:bg-zinc-600 h-[1px] mx-3 my-2" /> <MyMenuItem id="print">Sign out</MyMenuItem> </Menu> </Popover> </MenuTrigger> </div> ); } function MyMenuItem(props: MenuItemProps) { return ( <MenuItem {...props} className="group flex w-full items-center rounded-md px-3 py-2 box-border outline-hidden cursor-default text-gray-900 dark:text-gray-100 focus:bg-blue-500 focus:text-white" /> ); } function MySwitch(props: SwitchProps) { return ( <Switch className="group relative flex gap-2 items-center text-gray-800 dark:text-zinc-200 text-base transition"> {composeRenderProps(props.children, (children) => ( <> <div className="flex h-3 w-6 p-[2px] items-center shrink-0 cursor-default rounded-full transition duration-200 ease-in-out shadow-inner border border-transparent bg-gray-400 dark:bg-zinc-400 group-pressed:bg-gray-500 dark:group-pressed:bg-zinc-300 group-selected:bg-gray-700 group-selected:dark:bg-zinc-300 group-selected:forced-colors:bg-[Highlight]! group-selected:group-pressed:bg-gray-800 group-selected:dark:group-pressed:bg-zinc-200 outline outline-0 outline-blue-600 dark:outline-blue-500 forced-colors:outline-[Highlight] outline-offset-2 group-focus-visible:outline-2"> <div className="h-3 w-3 transform rounded-full bg-white dark:bg-zinc-900 outline outline-1 -outline-offset-1 outline-transparent shadow-sm transition duration-200 ease-in-out translate-x-0 group-selected:translate-x-[100%]" /> </div> {children} </> ))} </Switch> ); } import { Button, composeRenderProps, Menu, MenuItem, MenuTrigger, Popover, Separator, Switch } from 'react-aria-components'; import type { MenuItemProps, SwitchProps } from 'react-aria-components'; function AccountMenuExample() { return ( <div className="p-8 bg-gray-50 dark:bg-zinc-900 rounded-lg flex items-start justify-center"> <MenuTrigger> <Button aria-label="Account" className="inline-flex items-center justify-center rounded-md p-1.5 text-white bg-transparent border-none hover:bg-gray-200 pressed:bg-gray-300 dark:hover:bg-zinc-800 dark:pressed:bg-zinc-700 transition-colors cursor-default outline-hidden focus-visible:ring-2 focus-visible:ring-blue-600" > <img alt="" src="https://i.imgur.com/xIe7Wlb.png" className="w-7 h-7 rounded-full" /> </Button> <Popover className="p-2 overflow-auto outline-hidden rounded-lg bg-white dark:bg-zinc-950 shadow-lg ring-1 ring-black/10 dark:ring-white/15 entering:animate-in entering:fade-in entering:placement-bottom:slide-in-from-top-1 entering:placement-top:slide-in-from-bottom-1 exiting:animate-out exiting:fade-out exiting:placement-bottom:slide-out-to-top-1 exiting:placement-top:slide-out-to-bottom-1 fill-mode-forwards origin-top-left"> <div className="flex gap-2 items-center mx-3 mt-2"> <img alt="" src="https://i.imgur.com/xIe7Wlb.png" className="w-16 h-16 rounded-full" /> <div className="flex flex-col gap-1"> <div className="text-[15px] font-bold text-gray-900 dark:text-gray-100 leading-none"> Marissa Whitaker </div> <div className="text-base text-gray-900 dark:text-gray-100 leading-none mb-1"> user@example.com </div> <MySwitch> Dark Mode </MySwitch> </div> </div> <Separator className="border-none bg-gray-300 dark:bg-zinc-600 h-[1px] mx-3 mt-4 mb-2" /> <Menu className="outline-hidden"> <MyMenuItem id="new"> Account Settings </MyMenuItem> <MyMenuItem id="open"> Support </MyMenuItem> <Separator className="bg-gray-300 dark:bg-zinc-600 h-[1px] mx-3 my-2" /> <MyMenuItem id="save"> Legal notices </MyMenuItem> <MyMenuItem id="save-as"> About </MyMenuItem> <Separator className="bg-gray-300 dark:bg-zinc-600 h-[1px] mx-3 my-2" /> <MyMenuItem id="print"> Sign out </MyMenuItem> </Menu> </Popover> </MenuTrigger> </div> ); } function MyMenuItem( props: MenuItemProps ) { return ( <MenuItem {...props} className="group flex w-full items-center rounded-md px-3 py-2 box-border outline-hidden cursor-default text-gray-900 dark:text-gray-100 focus:bg-blue-500 focus:text-white" /> ); } function MySwitch( props: SwitchProps ) { return ( <Switch className="group relative flex gap-2 items-center text-gray-800 dark:text-zinc-200 text-base transition"> {composeRenderProps( props.children, (children) => ( <> <div className="flex h-3 w-6 p-[2px] items-center shrink-0 cursor-default rounded-full transition duration-200 ease-in-out shadow-inner border border-transparent bg-gray-400 dark:bg-zinc-400 group-pressed:bg-gray-500 dark:group-pressed:bg-zinc-300 group-selected:bg-gray-700 group-selected:dark:bg-zinc-300 group-selected:forced-colors:bg-[Highlight]! group-selected:group-pressed:bg-gray-800 group-selected:dark:group-pressed:bg-zinc-200 outline outline-0 outline-blue-600 dark:outline-blue-500 forced-colors:outline-[Highlight] outline-offset-2 group-focus-visible:outline-2"> <div className="h-3 w-3 transform rounded-full bg-white dark:bg-zinc-900 outline outline-1 -outline-offset-1 outline-transparent shadow-sm transition duration-200 ease-in-out translate-x-0 group-selected:translate-x-[100%]" /> </div> {children} </> ) )} </Switch> ); } ### Tailwind config# This example uses the following plugins: * tailwindcss-react-aria-components * tailwindcss-animate When using Tailwind v4, add them to your CSS: @import "tailwindcss"; @plugin "tailwindcss-react-aria-components"; @plugin "tailwindcss-animate"; @import "tailwindcss"; @plugin "tailwindcss-react-aria-components"; @plugin "tailwindcss-animate"; @import "tailwindcss"; @plugin "tailwindcss-react-aria-components"; @plugin "tailwindcss-animate"; Tailwind v3 When using Tailwind v3, add the plugins to your `tailwind.config.js` instead: module.exports = { // ... plugins: [ require('tailwindcss-react-aria-components'), require('tailwindcss-animate') ] }; module.exports = { // ... plugins: [ require('tailwindcss-react-aria-components'), require('tailwindcss-animate') ] }; module.exports = { // ... plugins: [ require( 'tailwindcss-react-aria-components' ), require( 'tailwindcss-animate' ) ] }; **Note**: When using Tailwind v3, install `tailwindcss-react-aria-components` version 1.x instead of 2.x. ## Components# * * * Menu A menu displays a list of actions or options that a user can choose. Button A button allows a user to perform an action. Popover A popover displays content in context with a trigger element. Dialog A dialog is an overlay shown above other content in an application. --- ## Page: https://react-spectrum.adobe.com/react-aria/examples/action-menu.html React AriaExamples # Action Menu An animated Menu of actions, styled with Tailwind CSS. ## Example# * * * import {Button, Menu, MenuItem, MenuTrigger, Popover, Separator} from 'react-aria-components'; import type {MenuItemProps} from 'react-aria-components'; import RailIcon from '@spectrum-icons/workflow/Rail'; function MenuExample() { return ( <div className="bg-linear-to-r to-blue-500 from-violet-500 p-8 h-[250px] rounded-lg"> <MenuTrigger> <Button aria-label="Menu" className="inline-flex items-center justify-center rounded-md bg-black/20 bg-clip-padding border border-white/20 px-3 py-2 text-white hover:bg-black/30 pressed:bg-black/40 transition-colors cursor-default outline-hidden focus-visible:ring-2 focus-visible:ring-white/75" > <RailIcon size="S" /> </Button> <Popover className="p-1 w-56 overflow-auto rounded-md bg-white shadow-lg ring-1 ring-black/5 entering:animate-in entering:fade-in entering:zoom-in-95 exiting:animate-out exiting:fade-out exiting:zoom-out-95 fill-mode-forwards origin-top-left"> <Menu className="outline-hidden"> <ActionItem id="new">New…</ActionItem> <ActionItem id="open">Open…</ActionItem> <Separator className="bg-gray-300 h-[1px] mx-3 my-1" /> <ActionItem id="save">Save</ActionItem> <ActionItem id="save-as">Save as…</ActionItem> <Separator className="bg-gray-300 h-[1px] mx-3 my-1" /> <ActionItem id="print">Print…</ActionItem> </Menu> </Popover> </MenuTrigger> </div> ); } function ActionItem(props: MenuItemProps) { return ( <MenuItem {...props} className="group flex w-full items-center rounded-md px-3 py-2 box-border outline-hidden cursor-default text-gray-900 focus:bg-violet-500 focus:text-white" /> ); } import { Button, Menu, MenuItem, MenuTrigger, Popover, Separator } from 'react-aria-components'; import type {MenuItemProps} from 'react-aria-components'; import RailIcon from '@spectrum-icons/workflow/Rail'; function MenuExample() { return ( <div className="bg-linear-to-r to-blue-500 from-violet-500 p-8 h-[250px] rounded-lg"> <MenuTrigger> <Button aria-label="Menu" className="inline-flex items-center justify-center rounded-md bg-black/20 bg-clip-padding border border-white/20 px-3 py-2 text-white hover:bg-black/30 pressed:bg-black/40 transition-colors cursor-default outline-hidden focus-visible:ring-2 focus-visible:ring-white/75" > <RailIcon size="S" /> </Button> <Popover className="p-1 w-56 overflow-auto rounded-md bg-white shadow-lg ring-1 ring-black/5 entering:animate-in entering:fade-in entering:zoom-in-95 exiting:animate-out exiting:fade-out exiting:zoom-out-95 fill-mode-forwards origin-top-left"> <Menu className="outline-hidden"> <ActionItem id="new">New…</ActionItem> <ActionItem id="open">Open…</ActionItem> <Separator className="bg-gray-300 h-[1px] mx-3 my-1" /> <ActionItem id="save">Save</ActionItem> <ActionItem id="save-as">Save as…</ActionItem> <Separator className="bg-gray-300 h-[1px] mx-3 my-1" /> <ActionItem id="print">Print…</ActionItem> </Menu> </Popover> </MenuTrigger> </div> ); } function ActionItem(props: MenuItemProps) { return ( <MenuItem {...props} className="group flex w-full items-center rounded-md px-3 py-2 box-border outline-hidden cursor-default text-gray-900 focus:bg-violet-500 focus:text-white" /> ); } import { Button, Menu, MenuItem, MenuTrigger, Popover, Separator } from 'react-aria-components'; import type {MenuItemProps} from 'react-aria-components'; import RailIcon from '@spectrum-icons/workflow/Rail'; function MenuExample() { return ( <div className="bg-linear-to-r to-blue-500 from-violet-500 p-8 h-[250px] rounded-lg"> <MenuTrigger> <Button aria-label="Menu" className="inline-flex items-center justify-center rounded-md bg-black/20 bg-clip-padding border border-white/20 px-3 py-2 text-white hover:bg-black/30 pressed:bg-black/40 transition-colors cursor-default outline-hidden focus-visible:ring-2 focus-visible:ring-white/75" > <RailIcon size="S" /> </Button> <Popover className="p-1 w-56 overflow-auto rounded-md bg-white shadow-lg ring-1 ring-black/5 entering:animate-in entering:fade-in entering:zoom-in-95 exiting:animate-out exiting:fade-out exiting:zoom-out-95 fill-mode-forwards origin-top-left"> <Menu className="outline-hidden"> <ActionItem id="new"> New… </ActionItem> <ActionItem id="open"> Open… </ActionItem> <Separator className="bg-gray-300 h-[1px] mx-3 my-1" /> <ActionItem id="save"> Save </ActionItem> <ActionItem id="save-as"> Save as… </ActionItem> <Separator className="bg-gray-300 h-[1px] mx-3 my-1" /> <ActionItem id="print"> Print… </ActionItem> </Menu> </Popover> </MenuTrigger> </div> ); } function ActionItem( props: MenuItemProps ) { return ( <MenuItem {...props} className="group flex w-full items-center rounded-md px-3 py-2 box-border outline-hidden cursor-default text-gray-900 focus:bg-violet-500 focus:text-white" /> ); } ### Tailwind config# This example uses the following plugins: * tailwindcss-react-aria-components * tailwindcss-animate When using Tailwind v4, add them to your CSS: @import "tailwindcss"; @plugin "tailwindcss-react-aria-components"; @plugin "tailwindcss-animate"; @import "tailwindcss"; @plugin "tailwindcss-react-aria-components"; @plugin "tailwindcss-animate"; @import "tailwindcss"; @plugin "tailwindcss-react-aria-components"; @plugin "tailwindcss-animate"; Tailwind v3 When using Tailwind v3, add the plugins to your `tailwind.config.js` instead: module.exports = { // ... plugins: [ require('tailwindcss-react-aria-components'), require('tailwindcss-animate') ] }; module.exports = { // ... plugins: [ require('tailwindcss-react-aria-components'), require('tailwindcss-animate') ] }; module.exports = { // ... plugins: [ require( 'tailwindcss-react-aria-components' ), require( 'tailwindcss-animate' ) ] }; **Note**: When using Tailwind v3, install `tailwindcss-react-aria-components` version 1.x instead of 2.x. ## Components# * * * Menu A menu displays a list of actions or options that a user can choose. Button A button allows a user to perform an action. Popover A popover displays content in context with a trigger element. --- ## Page: https://react-spectrum.adobe.com/react-aria/examples/category-tabs.html React AriaExamples # Category Tabs An article category Tabs component styled with Tailwind CSS. ## Example# * * * Blog Releases Docs ### Taming the dragon: Accessible drag and drop We are excited to announce the release of drag and drop support in React Aria and React Spectrum! This includes a suite of hooks for implementing drag and drop interactions, with support for both mouse and touch, as well as full parity for keyboard and screen reader input. ### Date and Time Pickers for All We are very excited to announce the release of the React Aria and React Spectrum date and time picker components! This includes a full suite of fully featured components and hooks including calendars, date and time fields, and range pickers, all with a focus on internationalization and accessibility. It also includes @internationalized/date, a brand new framework-agnostic library for locale-aware date and time manipulation. ### Creating an accessible autocomplete experience After many months of research, development, and testing, we’re excited to announce that the React Spectrum ComboBox component and React Aria useComboBox hook are now available! In this post we'll take a deeper look into some of the challenges we faced when building an accessible and mobile friendly ComboBox. import {Link, Tab, TabList, TabPanel, Tabs} from 'react-aria-components'; import type {TabPanelProps, TabProps} from 'react-aria-components'; function TabsExample() { return ( <div className="bg-linear-to-r from-lime-600 to-emerald-600 py-8 px-2 sm:px-8 rounded-lg flex justify-center"> <Tabs className="w-full max-w-[300px]"> <TabList aria-label="Feeds" className="flex space-x-1 rounded-full bg-green-900/40 bg-clip-padding p-1 border border-solid border-white/30" > <MyTab id="blog">Blog</MyTab> <MyTab id="releases">Releases</MyTab> <MyTab id="docs">Docs</MyTab> </TabList> <MyTabPanel id="blog"> <div className="flex flex-col"> <Article title="Taming the dragon: Accessible drag and drop" summary="We are excited to announce the release of drag and drop support in React Aria and React Spectrum! This includes a suite of hooks for implementing drag and drop interactions, with support for both mouse and touch, as well as full parity for keyboard and screen reader input." /> <Article title="Date and Time Pickers for All" summary="We are very excited to announce the release of the React Aria and React Spectrum date and time picker components! This includes a full suite of fully featured components and hooks including calendars, date and time fields, and range pickers, all with a focus on internationalization and accessibility. It also includes @internationalized/date, a brand new framework-agnostic library for locale-aware date and time manipulation." /> <Article title="Creating an accessible autocomplete experience" summary="After many months of research, development, and testing, we’re excited to announce that the React Spectrum ComboBox component and React Aria useComboBox hook are now available! In this post we'll take a deeper look into some of the challenges we faced when building an accessible and mobile friendly ComboBox." /> </div> </MyTabPanel> <MyTabPanel id="releases"> <div className="flex flex-col"> <Article title="February 23, 2023 Release" summary="In this release, we have added support for Node ESM to all of our packages. We have also been busy at work on our pre-releases and improving our focus management in collections." /> <Article title="December 16, 2022 Release" summary="It is our last release of the year and we are happy to share a new TableView feature, now in beta. Using the new allowsResizing prop on a Column in TableView gives users the ability to dynamically adjust the width of that column. TableView column resizing supports mouse, keyboard, touch, and screen reader interactions to allow all users to take advantage of a customizable table." /> <Article title="November 15, 2022 Release" summary="We are excited to announce the release of drag and drop support in React Aria and React Spectrum! This includes a suite of hooks for implementing drag and drop interactions. There is also an update to all Spectrum colors, aligning React Spectrum with the latest Spectrum designs. Finally, React Aria includes a new simplified API for overlays such as popovers and modals." /> </div> </MyTabPanel> <MyTabPanel id="docs"> <div className="flex flex-col"> <Article title="React Stately" summary="A library of React Hooks that provides cross-platform state management for your design system." /> <Article title="React Aria" summary="A library of React Hooks that provides accessible UI primitives for your design system." /> <Article title="React Spectrum" summary="A React implementation of Spectrum, Adobe’s design system." /> </div> </MyTabPanel> </Tabs> </div> ); } function MyTab(props: TabProps) { return ( <Tab {...props} className={({ isSelected }) => ` w-full rounded-full py-2.5 font-medium text-[1.1em] text-center cursor-default ring-black outline-hidden transition-colors focus-visible:ring-2 ${ isSelected ? 'text-emerald-700 bg-white shadow-sm' : 'text-white hover:bg-white/10 pressed:bg-white/10' } `} /> ); } function MyTabPanel(props: TabPanelProps) { return ( <TabPanel {...props} className="mt-2 text-gray-700 font-serif rounded-2xl bg-white p-2 shadow-sm ring-black outline-hidden focus-visible:ring-2" /> ); } function Article({ title, summary }: { title: string; summary: string }) { return ( <Link href="#" className="p-2 rounded-lg hover:bg-gray-100 pressed:bg-gray-100 text-[inherit] no-underline outline-hidden focus-visible:ring-2 ring-emerald-500" > <h3 className="text-base mt-0 mb-0.5 font-semibold overflow-hidden text-ellipsis whitespace-nowrap"> {title} </h3> <p className="text-sm m-0 overflow-hidden text-ellipsis line-clamp-2"> {summary} </p> </Link> ); } import { Link, Tab, TabList, TabPanel, Tabs } from 'react-aria-components'; import type { TabPanelProps, TabProps } from 'react-aria-components'; function TabsExample() { return ( <div className="bg-linear-to-r from-lime-600 to-emerald-600 py-8 px-2 sm:px-8 rounded-lg flex justify-center"> <Tabs className="w-full max-w-[300px]"> <TabList aria-label="Feeds" className="flex space-x-1 rounded-full bg-green-900/40 bg-clip-padding p-1 border border-solid border-white/30" > <MyTab id="blog">Blog</MyTab> <MyTab id="releases">Releases</MyTab> <MyTab id="docs">Docs</MyTab> </TabList> <MyTabPanel id="blog"> <div className="flex flex-col"> <Article title="Taming the dragon: Accessible drag and drop" summary="We are excited to announce the release of drag and drop support in React Aria and React Spectrum! This includes a suite of hooks for implementing drag and drop interactions, with support for both mouse and touch, as well as full parity for keyboard and screen reader input." /> <Article title="Date and Time Pickers for All" summary="We are very excited to announce the release of the React Aria and React Spectrum date and time picker components! This includes a full suite of fully featured components and hooks including calendars, date and time fields, and range pickers, all with a focus on internationalization and accessibility. It also includes @internationalized/date, a brand new framework-agnostic library for locale-aware date and time manipulation." /> <Article title="Creating an accessible autocomplete experience" summary="After many months of research, development, and testing, we’re excited to announce that the React Spectrum ComboBox component and React Aria useComboBox hook are now available! In this post we'll take a deeper look into some of the challenges we faced when building an accessible and mobile friendly ComboBox." /> </div> </MyTabPanel> <MyTabPanel id="releases"> <div className="flex flex-col"> <Article title="February 23, 2023 Release" summary="In this release, we have added support for Node ESM to all of our packages. We have also been busy at work on our pre-releases and improving our focus management in collections." /> <Article title="December 16, 2022 Release" summary="It is our last release of the year and we are happy to share a new TableView feature, now in beta. Using the new allowsResizing prop on a Column in TableView gives users the ability to dynamically adjust the width of that column. TableView column resizing supports mouse, keyboard, touch, and screen reader interactions to allow all users to take advantage of a customizable table." /> <Article title="November 15, 2022 Release" summary="We are excited to announce the release of drag and drop support in React Aria and React Spectrum! This includes a suite of hooks for implementing drag and drop interactions. There is also an update to all Spectrum colors, aligning React Spectrum with the latest Spectrum designs. Finally, React Aria includes a new simplified API for overlays such as popovers and modals." /> </div> </MyTabPanel> <MyTabPanel id="docs"> <div className="flex flex-col"> <Article title="React Stately" summary="A library of React Hooks that provides cross-platform state management for your design system." /> <Article title="React Aria" summary="A library of React Hooks that provides accessible UI primitives for your design system." /> <Article title="React Spectrum" summary="A React implementation of Spectrum, Adobe’s design system." /> </div> </MyTabPanel> </Tabs> </div> ); } function MyTab(props: TabProps) { return ( <Tab {...props} className={({ isSelected }) => ` w-full rounded-full py-2.5 font-medium text-[1.1em] text-center cursor-default ring-black outline-hidden transition-colors focus-visible:ring-2 ${ isSelected ? 'text-emerald-700 bg-white shadow-sm' : 'text-white hover:bg-white/10 pressed:bg-white/10' } `} /> ); } function MyTabPanel(props: TabPanelProps) { return ( <TabPanel {...props} className="mt-2 text-gray-700 font-serif rounded-2xl bg-white p-2 shadow-sm ring-black outline-hidden focus-visible:ring-2" /> ); } function Article( { title, summary }: { title: string; summary: string } ) { return ( <Link href="#" className="p-2 rounded-lg hover:bg-gray-100 pressed:bg-gray-100 text-[inherit] no-underline outline-hidden focus-visible:ring-2 ring-emerald-500" > <h3 className="text-base mt-0 mb-0.5 font-semibold overflow-hidden text-ellipsis whitespace-nowrap"> {title} </h3> <p className="text-sm m-0 overflow-hidden text-ellipsis line-clamp-2"> {summary} </p> </Link> ); } import { Link, Tab, TabList, TabPanel, Tabs } from 'react-aria-components'; import type { TabPanelProps, TabProps } from 'react-aria-components'; function TabsExample() { return ( <div className="bg-linear-to-r from-lime-600 to-emerald-600 py-8 px-2 sm:px-8 rounded-lg flex justify-center"> <Tabs className="w-full max-w-[300px]"> <TabList aria-label="Feeds" className="flex space-x-1 rounded-full bg-green-900/40 bg-clip-padding p-1 border border-solid border-white/30" > <MyTab id="blog"> Blog </MyTab> <MyTab id="releases"> Releases </MyTab> <MyTab id="docs"> Docs </MyTab> </TabList> <MyTabPanel id="blog"> <div className="flex flex-col"> <Article title="Taming the dragon: Accessible drag and drop" summary="We are excited to announce the release of drag and drop support in React Aria and React Spectrum! This includes a suite of hooks for implementing drag and drop interactions, with support for both mouse and touch, as well as full parity for keyboard and screen reader input." /> <Article title="Date and Time Pickers for All" summary="We are very excited to announce the release of the React Aria and React Spectrum date and time picker components! This includes a full suite of fully featured components and hooks including calendars, date and time fields, and range pickers, all with a focus on internationalization and accessibility. It also includes @internationalized/date, a brand new framework-agnostic library for locale-aware date and time manipulation." /> <Article title="Creating an accessible autocomplete experience" summary="After many months of research, development, and testing, we’re excited to announce that the React Spectrum ComboBox component and React Aria useComboBox hook are now available! In this post we'll take a deeper look into some of the challenges we faced when building an accessible and mobile friendly ComboBox." /> </div> </MyTabPanel> <MyTabPanel id="releases"> <div className="flex flex-col"> <Article title="February 23, 2023 Release" summary="In this release, we have added support for Node ESM to all of our packages. We have also been busy at work on our pre-releases and improving our focus management in collections." /> <Article title="December 16, 2022 Release" summary="It is our last release of the year and we are happy to share a new TableView feature, now in beta. Using the new allowsResizing prop on a Column in TableView gives users the ability to dynamically adjust the width of that column. TableView column resizing supports mouse, keyboard, touch, and screen reader interactions to allow all users to take advantage of a customizable table." /> <Article title="November 15, 2022 Release" summary="We are excited to announce the release of drag and drop support in React Aria and React Spectrum! This includes a suite of hooks for implementing drag and drop interactions. There is also an update to all Spectrum colors, aligning React Spectrum with the latest Spectrum designs. Finally, React Aria includes a new simplified API for overlays such as popovers and modals." /> </div> </MyTabPanel> <MyTabPanel id="docs"> <div className="flex flex-col"> <Article title="React Stately" summary="A library of React Hooks that provides cross-platform state management for your design system." /> <Article title="React Aria" summary="A library of React Hooks that provides accessible UI primitives for your design system." /> <Article title="React Spectrum" summary="A React implementation of Spectrum, Adobe’s design system." /> </div> </MyTabPanel> </Tabs> </div> ); } function MyTab( props: TabProps ) { return ( <Tab {...props} className={( { isSelected } ) => ` w-full rounded-full py-2.5 font-medium text-[1.1em] text-center cursor-default ring-black outline-hidden transition-colors focus-visible:ring-2 ${ isSelected ? 'text-emerald-700 bg-white shadow-sm' : 'text-white hover:bg-white/10 pressed:bg-white/10' } `} /> ); } function MyTabPanel( props: TabPanelProps ) { return ( <TabPanel {...props} className="mt-2 text-gray-700 font-serif rounded-2xl bg-white p-2 shadow-sm ring-black outline-hidden focus-visible:ring-2" /> ); } function Article( { title, summary }: { title: string; summary: string; } ) { return ( <Link href="#" className="p-2 rounded-lg hover:bg-gray-100 pressed:bg-gray-100 text-[inherit] no-underline outline-hidden focus-visible:ring-2 ring-emerald-500" > <h3 className="text-base mt-0 mb-0.5 font-semibold overflow-hidden text-ellipsis whitespace-nowrap"> {title} </h3> <p className="text-sm m-0 overflow-hidden text-ellipsis line-clamp-2"> {summary} </p> </Link> ); } ### Tailwind config# This example uses the tailwindcss-react-aria-components plugin. When using Tailwind v4, add it to your CSS: @import "tailwindcss"; @plugin "tailwindcss-react-aria-components"; @import "tailwindcss"; @plugin "tailwindcss-react-aria-components"; @import "tailwindcss"; @plugin "tailwindcss-react-aria-components"; Tailwind v3 When using Tailwind v3, add the plugin to your `tailwind.config.js` instead: module.exports = { // ... plugins: [ require('tailwindcss-react-aria-components') ] }; module.exports = { // ... plugins: [ require('tailwindcss-react-aria-components') ] }; module.exports = { // ... plugins: [ require( 'tailwindcss-react-aria-components' ) ] }; **Note**: When using Tailwind v3, install `tailwindcss-react-aria-components` version 1.x instead of 2.x. ## Components# * * * Tabs Tabs organize content into multiple sections, and allow a user to view one at a time. Link A link allows a user to navigate to another page. --- ## Page: https://react-spectrum.adobe.com/react-aria/examples/command-palette.html React AriaExamples # Command Palette A Command Palette is an interface that allows users to quickly run commands or navigate to content within an application. ## Example# * * * This example uses the `Autocomplete` component from React Aria Components to filter a list of commands and display them in a `Menu`. The `TextField` is used to capture user input and filter the list of available commands. Tap to openType Ctrl + K or press here to open import {Autocomplete, Button, Dialog, DialogTrigger, Input, Menu, MenuItem, Modal, ModalOverlay, TextField, useFilter} from 'react-aria-components'; import {useEffect, useMemo, useState} from 'react'; function CommandPaletteExample() { let commands = [ { id: 'new-file', label: 'Create new file…' }, { id: 'new-folder', label: 'Create new folder…' }, { id: 'assign', label: 'Assign to…' }, { id: 'assign-me', label: 'Assign to me' }, { id: 'status', label: 'Change status…' }, { id: 'priority', label: 'Change priority…' }, { id: 'label-add', label: 'Add label…' }, { id: 'label-remove', label: 'Remove label…' } ]; let [isOpen, setOpen] = useState(false); let { contains } = useFilter({ sensitivity: 'base' }); let isMac = useMemo(() => /Mac/.test(navigator.platform), []); useEffect(() => { const handleKeyDown = (e) => { if (e.key === 'k' && (isMac ? e.metaKey : e.ctrlKey)) { e.preventDefault(); setOpen((prev) => !prev); } else if (e.key === 'Escape') { e.preventDefault(); setOpen(false); } }; document.addEventListener('keydown', handleKeyDown); return () => document.removeEventListener('keydown', handleKeyDown); }); return ( <div className="bg-linear-to-r from-indigo-500 to-violet-500 p-4 sm:p-8 h-[340px] rounded-lg flex items-center justify-center"> <DialogTrigger isOpen={isOpen} onOpenChange={setOpen}> <Button className="inline-flex items-center justify-center rounded-xl bg-black/20 bg-clip-padding border border-white/20 px-3 py-2 font-medium font-[inherit] text-sm sm:text-base text-white hover:bg-black/30 pressed:bg-black/40 transition-colors cursor-default outline-hidden focus-visible:ring-2 focus-visible:ring-white/75"> <span className="block sm:hidden">Tap to open</span> <span className="hidden sm:block"> Type{' '} <kbd className="px-2 py-1 m-1 text-xs font-semibold border border-gray-200 rounded-lg"> {isMac ? '⌘' : 'Ctrl'} </kbd>{' '} +{' '} <kbd className="px-2 py-1 m-1 text-xs font-semibold border border-gray-200 rounded-lg"> K </kbd>{' '} or press here to open </span> </Button> <ModalOverlay isDismissable className={({ isEntering, isExiting }) => ` fixed inset-0 z-10 overflow-y-auto bg-black/25 flex min-h-full items-start sm:items-center justify-center p-4 text-center ${isEntering ? 'animate-in fade-in duration-300 ease-out' : ''} ${isExiting ? 'animate-out fade-out duration-200 ease-in' : ''} `} > <Modal className={({ isEntering, isExiting }) => ` ${isEntering ? 'animate-in zoom-in-95 ease-out duration-300' : ''} ${isExiting ? 'animate-out zoom-out-95 ease-in duration-200' : ''} `} > <Dialog className="outline-hidden relative"> <div className="flex flex-col gap-1 w-[95vw] sm:w-[500px] max-w-full rounded-xl bg-white shadow-lg p-2"> <Autocomplete filter={contains}> <TextField aria-label="Search commands" className="flex flex-col px-3 py-2 rounded-md outline-none placeholder-white/70" > <Input autoFocus placeholder="Search commands…" className="border-none py-2 px-3 leading-5 text-gray-900 bg-transparent outline-hidden text-base focus-visible:ring-2 focus-visible:ring-violet-500 rounded-lg" /> </TextField> <Menu items={commands} className="mt-2 p-1 max-h-44 overflow-auto" > {({ label }) => <CommandItem>{label}</CommandItem>} </Menu> </Autocomplete> </div> </Dialog> </Modal> </ModalOverlay> </DialogTrigger> </div> ); } function CommandItem(props) { return ( <MenuItem {...props} className="group flex w-full items-center rounded-md px-3 py-2 box-border outline-none cursor-default text-gray-900 hover:bg-violet-100 pressed:bg-violet-200 focus:bg-violet-500 focus:text-white" /> ); } import { Autocomplete, Button, Dialog, DialogTrigger, Input, Menu, MenuItem, Modal, ModalOverlay, TextField, useFilter } from 'react-aria-components'; import {useEffect, useMemo, useState} from 'react'; function CommandPaletteExample() { let commands = [ { id: 'new-file', label: 'Create new file…' }, { id: 'new-folder', label: 'Create new folder…' }, { id: 'assign', label: 'Assign to…' }, { id: 'assign-me', label: 'Assign to me' }, { id: 'status', label: 'Change status…' }, { id: 'priority', label: 'Change priority…' }, { id: 'label-add', label: 'Add label…' }, { id: 'label-remove', label: 'Remove label…' } ]; let [isOpen, setOpen] = useState(false); let { contains } = useFilter({ sensitivity: 'base' }); let isMac = useMemo( () => /Mac/.test(navigator.platform), [] ); useEffect(() => { const handleKeyDown = (e) => { if ( e.key === 'k' && (isMac ? e.metaKey : e.ctrlKey) ) { e.preventDefault(); setOpen((prev) => !prev); } else if (e.key === 'Escape') { e.preventDefault(); setOpen(false); } }; document.addEventListener('keydown', handleKeyDown); return () => document.removeEventListener( 'keydown', handleKeyDown ); }); return ( <div className="bg-linear-to-r from-indigo-500 to-violet-500 p-4 sm:p-8 h-[340px] rounded-lg flex items-center justify-center"> <DialogTrigger isOpen={isOpen} onOpenChange={setOpen}> <Button className="inline-flex items-center justify-center rounded-xl bg-black/20 bg-clip-padding border border-white/20 px-3 py-2 font-medium font-[inherit] text-sm sm:text-base text-white hover:bg-black/30 pressed:bg-black/40 transition-colors cursor-default outline-hidden focus-visible:ring-2 focus-visible:ring-white/75"> <span className="block sm:hidden"> Tap to open </span> <span className="hidden sm:block"> Type{' '} <kbd className="px-2 py-1 m-1 text-xs font-semibold border border-gray-200 rounded-lg"> {isMac ? '⌘' : 'Ctrl'} </kbd>{' '} +{' '} <kbd className="px-2 py-1 m-1 text-xs font-semibold border border-gray-200 rounded-lg"> K </kbd>{' '} or press here to open </span> </Button> <ModalOverlay isDismissable className={({ isEntering, isExiting }) => ` fixed inset-0 z-10 overflow-y-auto bg-black/25 flex min-h-full items-start sm:items-center justify-center p-4 text-center ${ isEntering ? 'animate-in fade-in duration-300 ease-out' : '' } ${ isExiting ? 'animate-out fade-out duration-200 ease-in' : '' } `} > <Modal className={({ isEntering, isExiting }) => ` ${ isEntering ? 'animate-in zoom-in-95 ease-out duration-300' : '' } ${ isExiting ? 'animate-out zoom-out-95 ease-in duration-200' : '' } `} > <Dialog className="outline-hidden relative"> <div className="flex flex-col gap-1 w-[95vw] sm:w-[500px] max-w-full rounded-xl bg-white shadow-lg p-2"> <Autocomplete filter={contains}> <TextField aria-label="Search commands" className="flex flex-col px-3 py-2 rounded-md outline-none placeholder-white/70" > <Input autoFocus placeholder="Search commands…" className="border-none py-2 px-3 leading-5 text-gray-900 bg-transparent outline-hidden text-base focus-visible:ring-2 focus-visible:ring-violet-500 rounded-lg" /> </TextField> <Menu items={commands} className="mt-2 p-1 max-h-44 overflow-auto" > {({ label }) => ( <CommandItem>{label}</CommandItem> )} </Menu> </Autocomplete> </div> </Dialog> </Modal> </ModalOverlay> </DialogTrigger> </div> ); } function CommandItem(props) { return ( <MenuItem {...props} className="group flex w-full items-center rounded-md px-3 py-2 box-border outline-none cursor-default text-gray-900 hover:bg-violet-100 pressed:bg-violet-200 focus:bg-violet-500 focus:text-white" /> ); } import { Autocomplete, Button, Dialog, DialogTrigger, Input, Menu, MenuItem, Modal, ModalOverlay, TextField, useFilter } from 'react-aria-components'; import { useEffect, useMemo, useState } from 'react'; function CommandPaletteExample() { let commands = [ { id: 'new-file', label: 'Create new file…' }, { id: 'new-folder', label: 'Create new folder…' }, { id: 'assign', label: 'Assign to…' }, { id: 'assign-me', label: 'Assign to me' }, { id: 'status', label: 'Change status…' }, { id: 'priority', label: 'Change priority…' }, { id: 'label-add', label: 'Add label…' }, { id: 'label-remove', label: 'Remove label…' } ]; let [isOpen, setOpen] = useState(false); let { contains } = useFilter({ sensitivity: 'base' }); let isMac = useMemo( () => /Mac/.test( navigator .platform ), [] ); useEffect(() => { const handleKeyDown = (e) => { if ( e.key === 'k' && (isMac ? e.metaKey : e .ctrlKey) ) { e.preventDefault(); setOpen(( prev ) => !prev); } else if ( e.key === 'Escape' ) { e.preventDefault(); setOpen(false); } }; document .addEventListener( 'keydown', handleKeyDown ); return () => document .removeEventListener( 'keydown', handleKeyDown ); }); return ( <div className="bg-linear-to-r from-indigo-500 to-violet-500 p-4 sm:p-8 h-[340px] rounded-lg flex items-center justify-center"> <DialogTrigger isOpen={isOpen} onOpenChange={setOpen} > <Button className="inline-flex items-center justify-center rounded-xl bg-black/20 bg-clip-padding border border-white/20 px-3 py-2 font-medium font-[inherit] text-sm sm:text-base text-white hover:bg-black/30 pressed:bg-black/40 transition-colors cursor-default outline-hidden focus-visible:ring-2 focus-visible:ring-white/75"> <span className="block sm:hidden"> Tap to open </span> <span className="hidden sm:block"> Type{' '} <kbd className="px-2 py-1 m-1 text-xs font-semibold border border-gray-200 rounded-lg"> {isMac ? '⌘' : 'Ctrl'} </kbd>{' '} +{' '} <kbd className="px-2 py-1 m-1 text-xs font-semibold border border-gray-200 rounded-lg"> K </kbd>{' '} or press here to open </span> </Button> <ModalOverlay isDismissable className={( { isEntering, isExiting } ) => ` fixed inset-0 z-10 overflow-y-auto bg-black/25 flex min-h-full items-start sm:items-center justify-center p-4 text-center ${ isEntering ? 'animate-in fade-in duration-300 ease-out' : '' } ${ isExiting ? 'animate-out fade-out duration-200 ease-in' : '' } `} > <Modal className={( { isEntering, isExiting } ) => ` ${ isEntering ? 'animate-in zoom-in-95 ease-out duration-300' : '' } ${ isExiting ? 'animate-out zoom-out-95 ease-in duration-200' : '' } `} > <Dialog className="outline-hidden relative"> <div className="flex flex-col gap-1 w-[95vw] sm:w-[500px] max-w-full rounded-xl bg-white shadow-lg p-2"> <Autocomplete filter={contains} > <TextField aria-label="Search commands" className="flex flex-col px-3 py-2 rounded-md outline-none placeholder-white/70" > <Input autoFocus placeholder="Search commands…" className="border-none py-2 px-3 leading-5 text-gray-900 bg-transparent outline-hidden text-base focus-visible:ring-2 focus-visible:ring-violet-500 rounded-lg" /> </TextField> <Menu items={commands} className="mt-2 p-1 max-h-44 overflow-auto" > {( { label } ) => ( <CommandItem> {label} </CommandItem> )} </Menu> </Autocomplete> </div> </Dialog> </Modal> </ModalOverlay> </DialogTrigger> </div> ); } function CommandItem( props ) { return ( <MenuItem {...props} className="group flex w-full items-center rounded-md px-3 py-2 box-border outline-none cursor-default text-gray-900 hover:bg-violet-100 pressed:bg-violet-200 focus:bg-violet-500 focus:text-white" /> ); } ### Tailwind config# This example uses the following plugins: * tailwindcss-react-aria-components * tailwindcss-animate When using Tailwind v4, add them to your CSS: @import "tailwindcss"; @plugin "tailwindcss-react-aria-components"; @plugin "tailwindcss-animate"; @import "tailwindcss"; @plugin "tailwindcss-react-aria-components"; @plugin "tailwindcss-animate"; @import "tailwindcss"; @plugin "tailwindcss-react-aria-components"; @plugin "tailwindcss-animate"; Tailwind v3 When using Tailwind v3, add the plugins to your `tailwind.config.js` instead: module.exports = { // ... plugins: [ require('tailwindcss-react-aria-components'), require('tailwindcss-animate') ] }; module.exports = { // ... plugins: [ require('tailwindcss-react-aria-components'), require('tailwindcss-animate') ] }; module.exports = { // ... plugins: [ require( 'tailwindcss-react-aria-components' ), require( 'tailwindcss-animate' ) ] }; **Note**: When using Tailwind v3, install `tailwindcss-react-aria-components` version 1.x instead of 2.x. ## Components# * * * TextField A text field allows a user to enter a plain text value with a keyboard. Menu A menu displays a list of actions or options that a user can choose. --- ## Page: https://react-spectrum.adobe.com/react-aria/examples/contact-list.html React AriaExamples # Contact List A ListBox styled with Tailwind CSS, featuring sticky section headers and macOS-style multiple selection. ## Example# * * * Favorites Tony Baldwin@tony Julienne Langstrath@jlangstrath Roberto Gonzalez@rgonzalez All Contacts Gilberto Miguel@gilberto\_miguel Maia Pettegree@mpettegree Wade Redington@redington Kurtis Gurrado@kurtis Sonja Balmann@sbalmann Brent Mickelwright@brent\_m Charles Webb@cwebb import {Collection, Header, ListBox, ListBoxItem, ListBoxSection, Text} from 'react-aria-components'; function ContactListExample() { return ( <div className="bg-linear-to-r from-blue-500 to-sky-500 p-8 rounded-lg flex justify-center"> <ListBox aria-label="Contacts" selectionMode="multiple" selectionBehavior="replace" className="w-72 max-h-[290px] overflow-auto outline-hidden bg-white text-gray-700 p-2 flex flex-col gap-2 rounded-lg shadow-sm scroll-pb-2 scroll-pt-7" > <ContactSection title="Favorites" items={favorites}> {(item) => <Contact item={item} />} </ContactSection> <ContactSection title="All Contacts" items={people}> {(item) => <Contact item={item} />} </ContactSection> </ListBox> </div> ); } function ContactSection({ title, children, items }) { return ( <ListBoxSection> <Header className="sticky -top-2 bg-white z-10 font-bold font-serif px-2 mb-1 text-slate-700"> {title} </Header> <Collection items={items}> {children} </Collection> </ListBoxSection> ); } function Contact({ item }) { return ( <ListBoxItem id={item.id} textValue={item.name} className="group relative py-1 px-2 outline-hidden cursor-default grid grid-rows-2 grid-flow-col auto-cols-max gap-x-3 rounded-sm selected:bg-blue-500 text-slate-700 selected:text-white [&:has(+[data-selected])]:selected:rounded-b-none [&[data-selected]+[data-selected]]:rounded-t-none focus-visible:ring-2 ring-offset-2 ring-blue-500" > <img src={item.avatar} alt="" className="row-span-2 place-self-center h-8 w-8 rounded-full" /> <Text slot="label" className="font-semibold truncate">{item.name}</Text> <Text slot="description" className="truncate text-sm text-slate-600 group-selected:text-white" > {item.username} </Text> <div className="absolute left-12 right-2 bottom-0 h-px bg-gray-200 group-selected:bg-blue-400 [.group[data-selected]:has(+:not([data-selected]))_&]:hidden [.group:not([data-selected]):has(+[data-selected])_&]:hidden [.group[data-selected]:last-child_&]:hidden" /> </ListBoxItem> ); } import { Collection, Header, ListBox, ListBoxItem, ListBoxSection, Text } from 'react-aria-components'; function ContactListExample() { return ( <div className="bg-linear-to-r from-blue-500 to-sky-500 p-8 rounded-lg flex justify-center"> <ListBox aria-label="Contacts" selectionMode="multiple" selectionBehavior="replace" className="w-72 max-h-[290px] overflow-auto outline-hidden bg-white text-gray-700 p-2 flex flex-col gap-2 rounded-lg shadow-sm scroll-pb-2 scroll-pt-7" > <ContactSection title="Favorites" items={favorites}> {(item) => <Contact item={item} />} </ContactSection> <ContactSection title="All Contacts" items={people}> {(item) => <Contact item={item} />} </ContactSection> </ListBox> </div> ); } function ContactSection({ title, children, items }) { return ( <ListBoxSection> <Header className="sticky -top-2 bg-white z-10 font-bold font-serif px-2 mb-1 text-slate-700"> {title} </Header> <Collection items={items}> {children} </Collection> </ListBoxSection> ); } function Contact({ item }) { return ( <ListBoxItem id={item.id} textValue={item.name} className="group relative py-1 px-2 outline-hidden cursor-default grid grid-rows-2 grid-flow-col auto-cols-max gap-x-3 rounded-sm selected:bg-blue-500 text-slate-700 selected:text-white [&:has(+[data-selected])]:selected:rounded-b-none [&[data-selected]+[data-selected]]:rounded-t-none focus-visible:ring-2 ring-offset-2 ring-blue-500" > <img src={item.avatar} alt="" className="row-span-2 place-self-center h-8 w-8 rounded-full" /> <Text slot="label" className="font-semibold truncate"> {item.name} </Text> <Text slot="description" className="truncate text-sm text-slate-600 group-selected:text-white" > {item.username} </Text> <div className="absolute left-12 right-2 bottom-0 h-px bg-gray-200 group-selected:bg-blue-400 [.group[data-selected]:has(+:not([data-selected]))_&]:hidden [.group:not([data-selected]):has(+[data-selected])_&]:hidden [.group[data-selected]:last-child_&]:hidden" /> </ListBoxItem> ); } import { Collection, Header, ListBox, ListBoxItem, ListBoxSection, Text } from 'react-aria-components'; function ContactListExample() { return ( <div className="bg-linear-to-r from-blue-500 to-sky-500 p-8 rounded-lg flex justify-center"> <ListBox aria-label="Contacts" selectionMode="multiple" selectionBehavior="replace" className="w-72 max-h-[290px] overflow-auto outline-hidden bg-white text-gray-700 p-2 flex flex-col gap-2 rounded-lg shadow-sm scroll-pb-2 scroll-pt-7" > <ContactSection title="Favorites" items={favorites} > {(item) => ( <Contact item={item} /> )} </ContactSection> <ContactSection title="All Contacts" items={people} > {(item) => ( <Contact item={item} /> )} </ContactSection> </ListBox> </div> ); } function ContactSection( { title, children, items } ) { return ( <ListBoxSection> <Header className="sticky -top-2 bg-white z-10 font-bold font-serif px-2 mb-1 text-slate-700"> {title} </Header> <Collection items={items} > {children} </Collection> </ListBoxSection> ); } function Contact( { item } ) { return ( <ListBoxItem id={item.id} textValue={item .name} className="group relative py-1 px-2 outline-hidden cursor-default grid grid-rows-2 grid-flow-col auto-cols-max gap-x-3 rounded-sm selected:bg-blue-500 text-slate-700 selected:text-white [&:has(+[data-selected])]:selected:rounded-b-none [&[data-selected]+[data-selected]]:rounded-t-none focus-visible:ring-2 ring-offset-2 ring-blue-500" > <img src={item.avatar} alt="" className="row-span-2 place-self-center h-8 w-8 rounded-full" /> <Text slot="label" className="font-semibold truncate" > {item.name} </Text> <Text slot="description" className="truncate text-sm text-slate-600 group-selected:text-white" > {item.username} </Text> <div className="absolute left-12 right-2 bottom-0 h-px bg-gray-200 group-selected:bg-blue-400 [.group[data-selected]:has(+:not([data-selected]))_&]:hidden [.group:not([data-selected]):has(+[data-selected])_&]:hidden [.group[data-selected]:last-child_&]:hidden" /> </ListBoxItem> ); } ### Tailwind config# This example uses the tailwindcss-react-aria-components plugin. When using Tailwind v4, add it to your CSS: @import "tailwindcss"; @plugin "tailwindcss-react-aria-components"; @import "tailwindcss"; @plugin "tailwindcss-react-aria-components"; @import "tailwindcss"; @plugin "tailwindcss-react-aria-components"; Tailwind v3 When using Tailwind v3, add the plugin to your `tailwind.config.js` instead: module.exports = { // ... plugins: [ require('tailwindcss-react-aria-components') ] }; module.exports = { // ... plugins: [ require('tailwindcss-react-aria-components') ] }; module.exports = { // ... plugins: [ require( 'tailwindcss-react-aria-components' ) ] }; **Note**: When using Tailwind v3, install `tailwindcss-react-aria-components` version 1.x instead of 2.x. ## Components# * * * ListBox A listbox displays a list of options, and allows a user to select one or more of them. --- ## Page: https://react-spectrum.adobe.com/react-aria/examples/datepicker.html React AriaExamples # DatePicker A DatePicker component styled with Tailwind CSS. ## Example# * * * Date mm/dd/yyyy import {Button, Calendar, CalendarCell, CalendarGrid, CalendarGridBody, CalendarGridHeader, CalendarHeaderCell, DateInput, DatePicker, DateSegment, Dialog, Group, Heading, Label, Popover} from 'react-aria-components'; import type {ButtonProps, PopoverProps} from 'react-aria-components'; import ChevronUpDownIcon from '@spectrum-icons/workflow/ChevronUpDown'; import ChevronLeftIcon from '@spectrum-icons/workflow/ChevronLeft'; import ChevronRightIcon from '@spectrum-icons/workflow/ChevronRight'; function DatePickerExample() { return ( <div className="bg-linear-to-r from-violet-500 to-fuchsia-600 p-12 sm:h-[400px] rounded-lg flex items-start justify-center"> <DatePicker className="group flex flex-col gap-1 w-[200px]"> <Label className="text-white cursor-default">Date</Label> <Group className="flex rounded-lg bg-white/90 focus-within:bg-white group-open:bg-white transition pl-3 shadow-md text-gray-700 focus-visible:ring-2 ring-black"> <DateInput className="flex flex-1 py-2"> {(segment) => ( <DateSegment segment={segment} className="px-0.5 tabular-nums outline-hidden rounded-xs focus:bg-violet-700 focus:text-white caret-transparent placeholder-shown:italic" /> )} </DateInput> <Button className="outline-hidden px-3 flex items-center text-gray-700 transition border-0 border-solid border-l border-l-purple-200 bg-transparent rounded-r-lg pressed:bg-purple-100 focus-visible:ring-2 ring-black"> <ChevronUpDownIcon size="XS" /> </Button> </Group> <MyPopover> <Dialog className="p-6 text-gray-600"> <Calendar> <header className="flex items-center gap-1 pb-4 px-1 font-serif w-full"> <Heading className="flex-1 font-semibold text-2xl ml-2" /> <RoundButton slot="previous"> <ChevronLeftIcon /> </RoundButton> <RoundButton slot="next"> <ChevronRightIcon /> </RoundButton> </header> <CalendarGrid className="border-spacing-1 border-separate"> <CalendarGridHeader> {(day) => ( <CalendarHeaderCell className="text-xs text-gray-500 font-semibold"> {day} </CalendarHeaderCell> )} </CalendarGridHeader> <CalendarGridBody> {(date) => ( <CalendarCell date={date} className="w-9 h-9 outline-hidden cursor-default rounded-full flex items-center justify-center outside-month:text-gray-300 hover:bg-gray-100 pressed:bg-gray-200 selected:bg-violet-700 selected:text-white focus-visible:ring-3 ring-violet-600/70 ring-offset-2" /> )} </CalendarGridBody> </CalendarGrid> </Calendar> </Dialog> </MyPopover> </DatePicker> </div> ); } function RoundButton(props: ButtonProps) { return ( <Button {...props} className="w-9 h-9 outline-hidden cursor-default bg-transparent text-gray-600 border-0 rounded-full flex items-center justify-center hover:bg-gray-100 pressed:bg-gray-200 focus-visible:ring-3 ring-violet-600/70 ring-offset-2" /> ); } function MyPopover(props: PopoverProps) { return ( <Popover {...props} className={({ isEntering, isExiting }) => ` overflow-auto rounded-lg drop-shadow-lg ring-1 ring-black/10 bg-white ${ isEntering ? 'animate-in fade-in placement-bottom:slide-in-from-top-1 placement-top:slide-in-from-bottom-1 ease-out duration-200' : '' } ${ isExiting ? 'animate-out fade-out placement-bottom:slide-out-to-top-1 placement-top:slide-out-to-bottom-1 ease-in duration-150' : '' } `} /> ); } import { Button, Calendar, CalendarCell, CalendarGrid, CalendarGridBody, CalendarGridHeader, CalendarHeaderCell, DateInput, DatePicker, DateSegment, Dialog, Group, Heading, Label, Popover } from 'react-aria-components'; import type { ButtonProps, PopoverProps } from 'react-aria-components'; import ChevronUpDownIcon from '@spectrum-icons/workflow/ChevronUpDown'; import ChevronLeftIcon from '@spectrum-icons/workflow/ChevronLeft'; import ChevronRightIcon from '@spectrum-icons/workflow/ChevronRight'; function DatePickerExample() { return ( <div className="bg-linear-to-r from-violet-500 to-fuchsia-600 p-12 sm:h-[400px] rounded-lg flex items-start justify-center"> <DatePicker className="group flex flex-col gap-1 w-[200px]"> <Label className="text-white cursor-default"> Date </Label> <Group className="flex rounded-lg bg-white/90 focus-within:bg-white group-open:bg-white transition pl-3 shadow-md text-gray-700 focus-visible:ring-2 ring-black"> <DateInput className="flex flex-1 py-2"> {(segment) => ( <DateSegment segment={segment} className="px-0.5 tabular-nums outline-hidden rounded-xs focus:bg-violet-700 focus:text-white caret-transparent placeholder-shown:italic" /> )} </DateInput> <Button className="outline-hidden px-3 flex items-center text-gray-700 transition border-0 border-solid border-l border-l-purple-200 bg-transparent rounded-r-lg pressed:bg-purple-100 focus-visible:ring-2 ring-black"> <ChevronUpDownIcon size="XS" /> </Button> </Group> <MyPopover> <Dialog className="p-6 text-gray-600"> <Calendar> <header className="flex items-center gap-1 pb-4 px-1 font-serif w-full"> <Heading className="flex-1 font-semibold text-2xl ml-2" /> <RoundButton slot="previous"> <ChevronLeftIcon /> </RoundButton> <RoundButton slot="next"> <ChevronRightIcon /> </RoundButton> </header> <CalendarGrid className="border-spacing-1 border-separate"> <CalendarGridHeader> {(day) => ( <CalendarHeaderCell className="text-xs text-gray-500 font-semibold"> {day} </CalendarHeaderCell> )} </CalendarGridHeader> <CalendarGridBody> {(date) => ( <CalendarCell date={date} className="w-9 h-9 outline-hidden cursor-default rounded-full flex items-center justify-center outside-month:text-gray-300 hover:bg-gray-100 pressed:bg-gray-200 selected:bg-violet-700 selected:text-white focus-visible:ring-3 ring-violet-600/70 ring-offset-2" /> )} </CalendarGridBody> </CalendarGrid> </Calendar> </Dialog> </MyPopover> </DatePicker> </div> ); } function RoundButton(props: ButtonProps) { return ( <Button {...props} className="w-9 h-9 outline-hidden cursor-default bg-transparent text-gray-600 border-0 rounded-full flex items-center justify-center hover:bg-gray-100 pressed:bg-gray-200 focus-visible:ring-3 ring-violet-600/70 ring-offset-2" /> ); } function MyPopover(props: PopoverProps) { return ( <Popover {...props} className={({ isEntering, isExiting }) => ` overflow-auto rounded-lg drop-shadow-lg ring-1 ring-black/10 bg-white ${ isEntering ? 'animate-in fade-in placement-bottom:slide-in-from-top-1 placement-top:slide-in-from-bottom-1 ease-out duration-200' : '' } ${ isExiting ? 'animate-out fade-out placement-bottom:slide-out-to-top-1 placement-top:slide-out-to-bottom-1 ease-in duration-150' : '' } `} /> ); } import { Button, Calendar, CalendarCell, CalendarGrid, CalendarGridBody, CalendarGridHeader, CalendarHeaderCell, DateInput, DatePicker, DateSegment, Dialog, Group, Heading, Label, Popover } from 'react-aria-components'; import type { ButtonProps, PopoverProps } from 'react-aria-components'; import ChevronUpDownIcon from '@spectrum-icons/workflow/ChevronUpDown'; import ChevronLeftIcon from '@spectrum-icons/workflow/ChevronLeft'; import ChevronRightIcon from '@spectrum-icons/workflow/ChevronRight'; function DatePickerExample() { return ( <div className="bg-linear-to-r from-violet-500 to-fuchsia-600 p-12 sm:h-[400px] rounded-lg flex items-start justify-center"> <DatePicker className="group flex flex-col gap-1 w-[200px]"> <Label className="text-white cursor-default"> Date </Label> <Group className="flex rounded-lg bg-white/90 focus-within:bg-white group-open:bg-white transition pl-3 shadow-md text-gray-700 focus-visible:ring-2 ring-black"> <DateInput className="flex flex-1 py-2"> {( segment ) => ( <DateSegment segment={segment} className="px-0.5 tabular-nums outline-hidden rounded-xs focus:bg-violet-700 focus:text-white caret-transparent placeholder-shown:italic" /> )} </DateInput> <Button className="outline-hidden px-3 flex items-center text-gray-700 transition border-0 border-solid border-l border-l-purple-200 bg-transparent rounded-r-lg pressed:bg-purple-100 focus-visible:ring-2 ring-black"> <ChevronUpDownIcon size="XS" /> </Button> </Group> <MyPopover> <Dialog className="p-6 text-gray-600"> <Calendar> <header className="flex items-center gap-1 pb-4 px-1 font-serif w-full"> <Heading className="flex-1 font-semibold text-2xl ml-2" /> <RoundButton slot="previous"> <ChevronLeftIcon /> </RoundButton> <RoundButton slot="next"> <ChevronRightIcon /> </RoundButton> </header> <CalendarGrid className="border-spacing-1 border-separate"> <CalendarGridHeader> {(day) => ( <CalendarHeaderCell className="text-xs text-gray-500 font-semibold"> {day} </CalendarHeaderCell> )} </CalendarGridHeader> <CalendarGridBody> {(date) => ( <CalendarCell date={date} className="w-9 h-9 outline-hidden cursor-default rounded-full flex items-center justify-center outside-month:text-gray-300 hover:bg-gray-100 pressed:bg-gray-200 selected:bg-violet-700 selected:text-white focus-visible:ring-3 ring-violet-600/70 ring-offset-2" /> )} </CalendarGridBody> </CalendarGrid> </Calendar> </Dialog> </MyPopover> </DatePicker> </div> ); } function RoundButton( props: ButtonProps ) { return ( <Button {...props} className="w-9 h-9 outline-hidden cursor-default bg-transparent text-gray-600 border-0 rounded-full flex items-center justify-center hover:bg-gray-100 pressed:bg-gray-200 focus-visible:ring-3 ring-violet-600/70 ring-offset-2" /> ); } function MyPopover( props: PopoverProps ) { return ( <Popover {...props} className={( { isEntering, isExiting } ) => ` overflow-auto rounded-lg drop-shadow-lg ring-1 ring-black/10 bg-white ${ isEntering ? 'animate-in fade-in placement-bottom:slide-in-from-top-1 placement-top:slide-in-from-bottom-1 ease-out duration-200' : '' } ${ isExiting ? 'animate-out fade-out placement-bottom:slide-out-to-top-1 placement-top:slide-out-to-bottom-1 ease-in duration-150' : '' } `} /> ); } ### Tailwind config# This example uses the following plugins: * tailwindcss-react-aria-components * tailwindcss-animate When using Tailwind v4, add them to your CSS: @import "tailwindcss"; @plugin "tailwindcss-react-aria-components"; @plugin "tailwindcss-animate"; @import "tailwindcss"; @plugin "tailwindcss-react-aria-components"; @plugin "tailwindcss-animate"; @import "tailwindcss"; @plugin "tailwindcss-react-aria-components"; @plugin "tailwindcss-animate"; Tailwind v3 When using Tailwind v3, add the plugins to your `tailwind.config.js` instead: module.exports = { // ... plugins: [ require('tailwindcss-react-aria-components'), require('tailwindcss-animate') ] }; module.exports = { // ... plugins: [ require('tailwindcss-react-aria-components'), require('tailwindcss-animate') ] }; module.exports = { // ... plugins: [ require( 'tailwindcss-react-aria-components' ), require( 'tailwindcss-animate' ) ] }; **Note**: When using Tailwind v3, install `tailwindcss-react-aria-components` version 1.x instead of 2.x. ## Components# * * * DatePicker A date picker combines a DateField and a Calendar popover. DateField A date field allows a user to enter and edit date values using a keyboard. Calendar A calendar allows a user to select a single date from a date grid. Button A button allows a user to perform an action. Popover A popover displays content in context with a trigger element. Dialog A dialog is an overlay shown above other content in an application. --- ## Page: https://react-spectrum.adobe.com/react-aria/examples/destructive-dialog.html React AriaExamples # Destructive Alert Dialog An animated confirmation Dialog for a destructive action, styled with Tailwind CSS. ## Example# * * * Delete… import {Button, Dialog, DialogTrigger, Heading, Modal, ModalOverlay} from 'react-aria-components'; import AlertIcon from '@spectrum-icons/workflow/Alert'; function ModalExample() { return ( <div className="bg-linear-to-r from-sky-400 to-indigo-500 p-12 rounded-lg flex justify-center"> <DialogTrigger> <Button className="inline-flex items-center justify-center rounded-md bg-black/20 bg-clip-padding border border-white/20 px-3.5 py-2 font-medium font-[inherit] text-base text-white hover:bg-black/30 pressed:bg-black/40 transition-colors cursor-default outline-hidden focus-visible:ring-2 focus-visible:ring-white/75"> Delete… </Button> <ModalOverlay className={({ isEntering, isExiting }) => ` fixed inset-0 z-10 overflow-y-auto bg-black/25 flex min-h-full items-center justify-center p-4 text-center backdrop-blur ${isEntering ? 'animate-in fade-in duration-300 ease-out' : ''} ${isExiting ? 'animate-out fade-out duration-200 ease-in' : ''} `} > <Modal className={({ isEntering, isExiting }) => ` w-full max-w-md overflow-hidden rounded-2xl bg-white p-6 text-left align-middle shadow-xl ${isEntering ? 'animate-in zoom-in-95 ease-out duration-300' : ''} ${isExiting ? 'animate-out zoom-out-95 ease-in duration-200' : ''} `} > <Dialog role="alertdialog" className="outline-hidden relative"> {({ close }) => ( <> <Heading slot="title" className="text-xxl font-semibold leading-6 my-0 text-slate-700" > Delete folder </Heading> <div className="w-6 h-6 text-red-500 absolute right-0 top-0 stroke-2"> <AlertIcon size="M" /> </div> <p className="mt-3 text-slate-500"> Are you sure you want to delete "Documents"? All contents will be permanently destroyed. </p> <div className="mt-6 flex justify-end gap-2"> <DialogButton className="bg-slate-200 text-slate-800 hover:border-slate-300 pressed:bg-slate-300" onPress={close} > Cancel </DialogButton> <DialogButton className="bg-red-500 text-white hover:border-red-600 pressed:bg-red-600" onPress={close} > Delete </DialogButton> </div> </> )} </Dialog> </Modal> </ModalOverlay> </DialogTrigger> </div> ); } function DialogButton({ className, ...props }) { return ( <Button {...props} className={`inline-flex justify-center rounded-md border border-solid border-transparent px-5 py-2 font-semibold font-[inherit] text-base transition-colors cursor-default outline-hidden focus-visible:ring-2 ring-blue-500 ring-offset-2 ${className}`} /> ); } import { Button, Dialog, DialogTrigger, Heading, Modal, ModalOverlay } from 'react-aria-components'; import AlertIcon from '@spectrum-icons/workflow/Alert'; function ModalExample() { return ( <div className="bg-linear-to-r from-sky-400 to-indigo-500 p-12 rounded-lg flex justify-center"> <DialogTrigger> <Button className="inline-flex items-center justify-center rounded-md bg-black/20 bg-clip-padding border border-white/20 px-3.5 py-2 font-medium font-[inherit] text-base text-white hover:bg-black/30 pressed:bg-black/40 transition-colors cursor-default outline-hidden focus-visible:ring-2 focus-visible:ring-white/75"> Delete… </Button> <ModalOverlay className={({ isEntering, isExiting }) => ` fixed inset-0 z-10 overflow-y-auto bg-black/25 flex min-h-full items-center justify-center p-4 text-center backdrop-blur ${ isEntering ? 'animate-in fade-in duration-300 ease-out' : '' } ${ isExiting ? 'animate-out fade-out duration-200 ease-in' : '' } `} > <Modal className={({ isEntering, isExiting }) => ` w-full max-w-md overflow-hidden rounded-2xl bg-white p-6 text-left align-middle shadow-xl ${ isEntering ? 'animate-in zoom-in-95 ease-out duration-300' : '' } ${ isExiting ? 'animate-out zoom-out-95 ease-in duration-200' : '' } `} > <Dialog role="alertdialog" className="outline-hidden relative" > {({ close }) => ( <> <Heading slot="title" className="text-xxl font-semibold leading-6 my-0 text-slate-700" > Delete folder </Heading> <div className="w-6 h-6 text-red-500 absolute right-0 top-0 stroke-2"> <AlertIcon size="M" /> </div> <p className="mt-3 text-slate-500"> Are you sure you want to delete "Documents"? All contents will be permanently destroyed. </p> <div className="mt-6 flex justify-end gap-2"> <DialogButton className="bg-slate-200 text-slate-800 hover:border-slate-300 pressed:bg-slate-300" onPress={close} > Cancel </DialogButton> <DialogButton className="bg-red-500 text-white hover:border-red-600 pressed:bg-red-600" onPress={close} > Delete </DialogButton> </div> </> )} </Dialog> </Modal> </ModalOverlay> </DialogTrigger> </div> ); } function DialogButton({ className, ...props }) { return ( <Button {...props} className={`inline-flex justify-center rounded-md border border-solid border-transparent px-5 py-2 font-semibold font-[inherit] text-base transition-colors cursor-default outline-hidden focus-visible:ring-2 ring-blue-500 ring-offset-2 ${className}`} /> ); } import { Button, Dialog, DialogTrigger, Heading, Modal, ModalOverlay } from 'react-aria-components'; import AlertIcon from '@spectrum-icons/workflow/Alert'; function ModalExample() { return ( <div className="bg-linear-to-r from-sky-400 to-indigo-500 p-12 rounded-lg flex justify-center"> <DialogTrigger> <Button className="inline-flex items-center justify-center rounded-md bg-black/20 bg-clip-padding border border-white/20 px-3.5 py-2 font-medium font-[inherit] text-base text-white hover:bg-black/30 pressed:bg-black/40 transition-colors cursor-default outline-hidden focus-visible:ring-2 focus-visible:ring-white/75"> Delete… </Button> <ModalOverlay className={( { isEntering, isExiting } ) => ` fixed inset-0 z-10 overflow-y-auto bg-black/25 flex min-h-full items-center justify-center p-4 text-center backdrop-blur ${ isEntering ? 'animate-in fade-in duration-300 ease-out' : '' } ${ isExiting ? 'animate-out fade-out duration-200 ease-in' : '' } `} > <Modal className={( { isEntering, isExiting } ) => ` w-full max-w-md overflow-hidden rounded-2xl bg-white p-6 text-left align-middle shadow-xl ${ isEntering ? 'animate-in zoom-in-95 ease-out duration-300' : '' } ${ isExiting ? 'animate-out zoom-out-95 ease-in duration-200' : '' } `} > <Dialog role="alertdialog" className="outline-hidden relative" > {( { close } ) => ( <> <Heading slot="title" className="text-xxl font-semibold leading-6 my-0 text-slate-700" > Delete folder </Heading> <div className="w-6 h-6 text-red-500 absolute right-0 top-0 stroke-2"> <AlertIcon size="M" /> </div> <p className="mt-3 text-slate-500"> Are you sure you want to delete "Documents"? All contents will be permanently destroyed. </p> <div className="mt-6 flex justify-end gap-2"> <DialogButton className="bg-slate-200 text-slate-800 hover:border-slate-300 pressed:bg-slate-300" onPress={close} > Cancel </DialogButton> <DialogButton className="bg-red-500 text-white hover:border-red-600 pressed:bg-red-600" onPress={close} > Delete </DialogButton> </div> </> )} </Dialog> </Modal> </ModalOverlay> </DialogTrigger> </div> ); } function DialogButton( { className, ...props } ) { return ( <Button {...props} className={`inline-flex justify-center rounded-md border border-solid border-transparent px-5 py-2 font-semibold font-[inherit] text-base transition-colors cursor-default outline-hidden focus-visible:ring-2 ring-blue-500 ring-offset-2 ${className}`} /> ); } ### Tailwind config# This example uses the following plugins: * tailwindcss-react-aria-components * tailwindcss-animate When using Tailwind v4, add them to your CSS: @import "tailwindcss"; @plugin "tailwindcss-react-aria-components"; @plugin "tailwindcss-animate"; @import "tailwindcss"; @plugin "tailwindcss-react-aria-components"; @plugin "tailwindcss-animate"; @import "tailwindcss"; @plugin "tailwindcss-react-aria-components"; @plugin "tailwindcss-animate"; Tailwind v3 When using Tailwind v3, add the plugins to your `tailwind.config.js` instead: module.exports = { // ... plugins: [ require('tailwindcss-react-aria-components'), require('tailwindcss-animate') ] }; module.exports = { // ... plugins: [ require('tailwindcss-react-aria-components'), require('tailwindcss-animate') ] }; module.exports = { // ... plugins: [ require( 'tailwindcss-react-aria-components' ), require( 'tailwindcss-animate' ) ] }; **Note**: When using Tailwind v3, install `tailwindcss-react-aria-components` version 1.x instead of 2.x. ## Components# * * * Dialog A dialog is an overlay shown above other content in an application. Button A button allows a user to perform an action. --- ## Page: https://react-spectrum.adobe.com/react-aria/examples/file-system.html React AriaExamples # File System Tree A file system Tree featuring multiple selection and styled with Tailwind CSS. ## Example# * * * Documents Photos Videos Music Movies import {Button, Collection, Tree, TreeItem, TreeItemContent} from 'react-aria-components'; import ChevronIcon from '@spectrum-icons/ui/ChevronRightMedium'; function FileSystemExample() { return ( <div className="bg-linear-to-r from-indigo-500 to-violet-500 p-8 rounded-lg flex items-center justify-center"> <Tree aria-label="File system" selectionMode="multiple" selectionBehavior="replace" items={filesystem} defaultExpandedKeys={['documents']} className={` border-separate border-spacing-0 w-60 h-100 bg-slate-900 overflow-auto rounded-lg shadow-lg`} > {function renderItem(item) { return ( <TreeItem textValue={item.name} className={`selected:bg-slate-500 text-white cursor-default group outline-hidden focus-visible:outline focus-visible:outline-2 focus-visible:outline-slate-600 focus-visible:-outline-offset-4 selected:focus-visible:outline-white`} > <TreeItemContent> {({ hasChildItems }) => ( <div className="flex items-center space-x-2 py-2 ps-[calc(calc(var(--tree-item-level)_-_1)_*_calc(var(--spacing)_*_3))]"> {hasChildItems ? ( <Button slot="chevron" className={`shrink-0 w-8 h-8 group-data-[expanded=true]:rotate-90 transition-rotate duration-200 inline-flex items-center justify-center bg-transparent border-0 me-0 cursor-default outline-hidden text-white`} > <ChevronIcon /> </Button> ) : <div className="shrink-0 w-8 h-8" />} <div>{item.name}</div> </div> )} </TreeItemContent> <Collection items={item.children}> {renderItem} </Collection> </TreeItem> ); }} </Tree> </div> ); } import { Button, Collection, Tree, TreeItem, TreeItemContent } from 'react-aria-components'; import ChevronIcon from '@spectrum-icons/ui/ChevronRightMedium'; function FileSystemExample() { return ( <div className="bg-linear-to-r from-indigo-500 to-violet-500 p-8 rounded-lg flex items-center justify-center"> <Tree aria-label="File system" selectionMode="multiple" selectionBehavior="replace" items={filesystem} defaultExpandedKeys={['documents']} className={` border-separate border-spacing-0 w-60 h-100 bg-slate-900 overflow-auto rounded-lg shadow-lg`} > {function renderItem(item) { return ( <TreeItem textValue={item.name} className={`selected:bg-slate-500 text-white cursor-default group outline-hidden focus-visible:outline focus-visible:outline-2 focus-visible:outline-slate-600 focus-visible:-outline-offset-4 selected:focus-visible:outline-white`} > <TreeItemContent> {({ hasChildItems }) => ( <div className="flex items-center space-x-2 py-2 ps-[calc(calc(var(--tree-item-level)_-_1)_*_calc(var(--spacing)_*_3))]"> {hasChildItems ? ( <Button slot="chevron" className={`shrink-0 w-8 h-8 group-data-[expanded=true]:rotate-90 transition-rotate duration-200 inline-flex items-center justify-center bg-transparent border-0 me-0 cursor-default outline-hidden text-white`} > <ChevronIcon /> </Button> ) : <div className="shrink-0 w-8 h-8" />} <div>{item.name}</div> </div> )} </TreeItemContent> <Collection items={item.children}> {renderItem} </Collection> </TreeItem> ); }} </Tree> </div> ); } import { Button, Collection, Tree, TreeItem, TreeItemContent } from 'react-aria-components'; import ChevronIcon from '@spectrum-icons/ui/ChevronRightMedium'; function FileSystemExample() { return ( <div className="bg-linear-to-r from-indigo-500 to-violet-500 p-8 rounded-lg flex items-center justify-center"> <Tree aria-label="File system" selectionMode="multiple" selectionBehavior="replace" items={filesystem} defaultExpandedKeys={[ 'documents' ]} className={` border-separate border-spacing-0 w-60 h-100 bg-slate-900 overflow-auto rounded-lg shadow-lg`} > {function renderItem( item ) { return ( <TreeItem textValue={item .name} className={`selected:bg-slate-500 text-white cursor-default group outline-hidden focus-visible:outline focus-visible:outline-2 focus-visible:outline-slate-600 focus-visible:-outline-offset-4 selected:focus-visible:outline-white`} > <TreeItemContent> {( { hasChildItems } ) => ( <div className="flex items-center space-x-2 py-2 ps-[calc(calc(var(--tree-item-level)_-_1)_*_calc(var(--spacing)_*_3))]"> {hasChildItems ? ( <Button slot="chevron" className={`shrink-0 w-8 h-8 group-data-[expanded=true]:rotate-90 transition-rotate duration-200 inline-flex items-center justify-center bg-transparent border-0 me-0 cursor-default outline-hidden text-white`} > <ChevronIcon /> </Button> ) : ( <div className="shrink-0 w-8 h-8" /> )} <div> {item .name} </div> </div> )} </TreeItemContent> <Collection items={item .children} > {renderItem} </Collection> </TreeItem> ); }} </Tree> </div> ); } ### Tailwind config# This example uses the tailwindcss-react-aria-components plugin. When using Tailwind v4, add it to your CSS: @import "tailwindcss"; @plugin "tailwindcss-react-aria-components"; @import "tailwindcss"; @plugin "tailwindcss-react-aria-components"; @import "tailwindcss"; @plugin "tailwindcss-react-aria-components"; Tailwind v3 When using Tailwind v3, add the plugin to your `tailwind.config.js` instead: module.exports = { // ... plugins: [ require('tailwindcss-react-aria-components') ] }; module.exports = { // ... plugins: [ require('tailwindcss-react-aria-components') ] }; module.exports = { // ... plugins: [ require( 'tailwindcss-react-aria-components' ) ] }; **Note**: When using Tailwind v3, install `tailwindcss-react-aria-components` version 1.x instead of 2.x. ## Components# * * * Tree A tree provides users with a way to navigate nested hierarchical information, with support for keyboard navigation and selection. --- ## Page: https://react-spectrum.adobe.com/react-aria/examples/framer-modal-sheet.html React AriaExamples # Gesture Driven Modal Sheet An iOS-style gesture driven modal sheet built with React Aria Components, Framer Motion, and Tailwind CSS. ## Example# * * * Open sheet import {animate, AnimatePresence, motion, useMotionTemplate, useMotionValue, useMotionValueEvent, useTransform} from 'framer-motion'; import {Button, Dialog, Heading, Modal, ModalOverlay} from 'react-aria-components'; import {useState} from 'react'; // Wrap React Aria modal components so they support framer-motion values. const MotionModal = motion(Modal); const MotionModalOverlay = motion(ModalOverlay); const inertiaTransition = { type: 'inertia' as const, bounceStiffness: 300, bounceDamping: 40, timeConstant: 300 }; const staticTransition = { duration: 0.5, ease: [0.32, 0.72, 0, 1] }; const SHEET_MARGIN = 34; const SHEET_RADIUS = 12; const root = document.body.firstChild as HTMLElement; function Sheet() { let [isOpen, setOpen] = useState(false); let h = window.innerHeight - SHEET_MARGIN; let y = useMotionValue(h); let bgOpacity = useTransform(y, [0, h], [0.4, 0]); let bg = useMotionTemplate`rgba(0, 0, 0, ${bgOpacity})`; // Scale the body down and adjust the border radius when the sheet is open. let bodyScale = useTransform( y, [0, h], [(window.innerWidth - SHEET_MARGIN) / window.innerWidth, 1] ); let bodyTranslate = useTransform(y, [0, h], [SHEET_MARGIN - SHEET_RADIUS, 0]); let bodyBorderRadius = useTransform(y, [0, h], [SHEET_RADIUS, 0]); useMotionValueEvent(bodyScale, 'change', (v) => root.style.scale = `${v}`); useMotionValueEvent( bodyTranslate, 'change', (v) => root.style.translate = `0 ${v}px` ); useMotionValueEvent( bodyBorderRadius, 'change', (v) => root.style.borderRadius = `${v}px` ); return ( <> <Button className="text-blue-600 text-lg font-semibold outline-hidden rounded-sm bg-transparent border-none pressed:text-blue-700 focus-visible:ring-3" onPress={() => setOpen(true)} > Open sheet </Button> <AnimatePresence> {isOpen && ( <MotionModalOverlay // Force the modal to be open when AnimatePresence renders it. isOpen onOpenChange={setOpen} className="fixed inset-0 z-10" style={{ backgroundColor: bg as any }} > <MotionModal className="bg-(--page-background) absolute bottom-0 w-full rounded-t-xl shadow-lg will-change-transform" initial={{ y: h }} animate={{ y: 0 }} exit={{ y: h }} transition={staticTransition} style={{ y, top: SHEET_MARGIN, // Extra padding at the bottom to account for rubber band scrolling. paddingBottom: window.screen.height }} drag="y" dragConstraints={{ top: 0 }} onDragEnd={(e, { offset, velocity }) => { if (offset.y > window.innerHeight * 0.75 || velocity.y > 10) { setOpen(false); } else { animate(y, 0, { ...inertiaTransition, min: 0, max: 0 }); } }} > {/* drag affordance */} <div className="mx-auto w-12 mt-2 h-1.5 rounded-full bg-gray-400" /> <Dialog className="px-4 pb-4 outline-hidden"> <div className="flex justify-end"> <Button className="text-blue-600 text-lg font-semibold mb-8 outline-hidden rounded-sm bg-transparent border-none pressed:text-blue-700 focus-visible:ring-3" onPress={() => setOpen(false)} > Done </Button> </div> <Heading slot="title" className="text-3xl font-semibold mb-4"> Modal sheet </Heading> <p className="text-lg mb-4"> This is a dialog with a custom modal overlay built with React Aria Components and Framer Motion. </p> <p className="text-lg"> Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean sit amet nisl blandit, pellentesque eros eu, scelerisque eros. Sed cursus urna at nunc lacinia dapibus. </p> </Dialog> </MotionModal> </MotionModalOverlay> )} </AnimatePresence> </> ); } import { animate, AnimatePresence, motion, useMotionTemplate, useMotionValue, useMotionValueEvent, useTransform } from 'framer-motion'; import { Button, Dialog, Heading, Modal, ModalOverlay } from 'react-aria-components'; import {useState} from 'react'; // Wrap React Aria modal components so they support framer-motion values. const MotionModal = motion(Modal); const MotionModalOverlay = motion(ModalOverlay); const inertiaTransition = { type: 'inertia' as const, bounceStiffness: 300, bounceDamping: 40, timeConstant: 300 }; const staticTransition = { duration: 0.5, ease: [0.32, 0.72, 0, 1] }; const SHEET_MARGIN = 34; const SHEET_RADIUS = 12; const root = document.body.firstChild as HTMLElement; function Sheet() { let [isOpen, setOpen] = useState(false); let h = window.innerHeight - SHEET_MARGIN; let y = useMotionValue(h); let bgOpacity = useTransform(y, [0, h], [0.4, 0]); let bg = useMotionTemplate`rgba(0, 0, 0, ${bgOpacity})`; // Scale the body down and adjust the border radius when the sheet is open. let bodyScale = useTransform( y, [0, h], [ (window.innerWidth - SHEET_MARGIN) / window.innerWidth, 1 ] ); let bodyTranslate = useTransform(y, [0, h], [ SHEET_MARGIN - SHEET_RADIUS, 0 ]); let bodyBorderRadius = useTransform(y, [0, h], [ SHEET_RADIUS, 0 ]); useMotionValueEvent( bodyScale, 'change', (v) => root.style.scale = `${v}` ); useMotionValueEvent( bodyTranslate, 'change', (v) => root.style.translate = `0 ${v}px` ); useMotionValueEvent( bodyBorderRadius, 'change', (v) => root.style.borderRadius = `${v}px` ); return ( <> <Button className="text-blue-600 text-lg font-semibold outline-hidden rounded-sm bg-transparent border-none pressed:text-blue-700 focus-visible:ring-3" onPress={() => setOpen(true)} > Open sheet </Button> <AnimatePresence> {isOpen && ( <MotionModalOverlay // Force the modal to be open when AnimatePresence renders it. isOpen onOpenChange={setOpen} className="fixed inset-0 z-10" style={{ backgroundColor: bg as any }} > <MotionModal className="bg-(--page-background) absolute bottom-0 w-full rounded-t-xl shadow-lg will-change-transform" initial={{ y: h }} animate={{ y: 0 }} exit={{ y: h }} transition={staticTransition} style={{ y, top: SHEET_MARGIN, // Extra padding at the bottom to account for rubber band scrolling. paddingBottom: window.screen.height }} drag="y" dragConstraints={{ top: 0 }} onDragEnd={(e, { offset, velocity }) => { if ( offset.y > window.innerHeight * 0.75 || velocity.y > 10 ) { setOpen(false); } else { animate(y, 0, { ...inertiaTransition, min: 0, max: 0 }); } }} > {/* drag affordance */} <div className="mx-auto w-12 mt-2 h-1.5 rounded-full bg-gray-400" /> <Dialog className="px-4 pb-4 outline-hidden"> <div className="flex justify-end"> <Button className="text-blue-600 text-lg font-semibold mb-8 outline-hidden rounded-sm bg-transparent border-none pressed:text-blue-700 focus-visible:ring-3" onPress={() => setOpen(false)} > Done </Button> </div> <Heading slot="title" className="text-3xl font-semibold mb-4" > Modal sheet </Heading> <p className="text-lg mb-4"> This is a dialog with a custom modal overlay built with React Aria Components and Framer Motion. </p> <p className="text-lg"> Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean sit amet nisl blandit, pellentesque eros eu, scelerisque eros. Sed cursus urna at nunc lacinia dapibus. </p> </Dialog> </MotionModal> </MotionModalOverlay> )} </AnimatePresence> </> ); } import { animate, AnimatePresence, motion, useMotionTemplate, useMotionValue, useMotionValueEvent, useTransform } from 'framer-motion'; import { Button, Dialog, Heading, Modal, ModalOverlay } from 'react-aria-components'; import {useState} from 'react'; // Wrap React Aria modal components so they support framer-motion values. const MotionModal = motion(Modal); const MotionModalOverlay = motion(ModalOverlay); const inertiaTransition = { type: 'inertia' as const, bounceStiffness: 300, bounceDamping: 40, timeConstant: 300 }; const staticTransition = { duration: 0.5, ease: [ 0.32, 0.72, 0, 1 ] }; const SHEET_MARGIN = 34; const SHEET_RADIUS = 12; const root = document .body .firstChild as HTMLElement; function Sheet() { let [isOpen, setOpen] = useState(false); let h = window.innerHeight - SHEET_MARGIN; let y = useMotionValue( h ); let bgOpacity = useTransform(y, [ 0, h ], [0.4, 0]); let bg = useMotionTemplate`rgba(0, 0, 0, ${bgOpacity})`; // Scale the body down and adjust the border radius when the sheet is open. let bodyScale = useTransform( y, [0, h], [ (window .innerWidth - SHEET_MARGIN) / window .innerWidth, 1 ] ); let bodyTranslate = useTransform(y, [ 0, h ], [ SHEET_MARGIN - SHEET_RADIUS, 0 ]); let bodyBorderRadius = useTransform(y, [ 0, h ], [ SHEET_RADIUS, 0 ]); useMotionValueEvent( bodyScale, 'change', (v) => root.style.scale = `${v}` ); useMotionValueEvent( bodyTranslate, 'change', (v) => root.style .translate = `0 ${v}px` ); useMotionValueEvent( bodyBorderRadius, 'change', (v) => root.style .borderRadius = `${v}px` ); return ( <> <Button className="text-blue-600 text-lg font-semibold outline-hidden rounded-sm bg-transparent border-none pressed:text-blue-700 focus-visible:ring-3" onPress={() => setOpen(true)} > Open sheet </Button> <AnimatePresence> {isOpen && ( <MotionModalOverlay // Force the modal to be open when AnimatePresence renders it. isOpen onOpenChange={setOpen} className="fixed inset-0 z-10" style={{ backgroundColor: bg as any }} > <MotionModal className="bg-(--page-background) absolute bottom-0 w-full rounded-t-xl shadow-lg will-change-transform" initial={{ y: h }} animate={{ y: 0 }} exit={{ y: h }} transition={staticTransition} style={{ y, top: SHEET_MARGIN, // Extra padding at the bottom to account for rubber band scrolling. paddingBottom: window .screen .height }} drag="y" dragConstraints={{ top: 0 }} onDragEnd={( e, { offset, velocity } ) => { if ( offset .y > window .innerHeight * 0.75 || velocity .y > 10 ) { setOpen( false ); } else { animate( y, 0, { ...inertiaTransition, min: 0, max: 0 } ); } }} > {/* drag affordance */} <div className="mx-auto w-12 mt-2 h-1.5 rounded-full bg-gray-400" /> <Dialog className="px-4 pb-4 outline-hidden"> <div className="flex justify-end"> <Button className="text-blue-600 text-lg font-semibold mb-8 outline-hidden rounded-sm bg-transparent border-none pressed:text-blue-700 focus-visible:ring-3" onPress={() => setOpen( false )} > Done </Button> </div> <Heading slot="title" className="text-3xl font-semibold mb-4" > Modal sheet </Heading> <p className="text-lg mb-4"> This is a dialog with a custom modal overlay built with React Aria Components and Framer Motion. </p> <p className="text-lg"> Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean sit amet nisl blandit, pellentesque eros eu, scelerisque eros. Sed cursus urna at nunc lacinia dapibus. </p> </Dialog> </MotionModal> </MotionModalOverlay> )} </AnimatePresence> </> ); } ### Tailwind config# This example uses the tailwindcss-react-aria-components plugin. When using Tailwind v4, add it to your CSS: @import "tailwindcss"; @plugin "tailwindcss-react-aria-components"; @import "tailwindcss"; @plugin "tailwindcss-react-aria-components"; @import "tailwindcss"; @plugin "tailwindcss-react-aria-components"; Tailwind v3 When using Tailwind v3, add the plugin to your `tailwind.config.js` instead: module.exports = { // ... plugins: [ require('tailwindcss-react-aria-components') ] }; module.exports = { // ... plugins: [ require('tailwindcss-react-aria-components') ] }; module.exports = { // ... plugins: [ require( 'tailwindcss-react-aria-components' ) ] }; **Note**: When using Tailwind v3, install `tailwindcss-react-aria-components` version 1.x instead of 2.x. ## Components# * * * Dialog A dialog is an overlay shown above other content in an application. Button A button allows a user to perform an action. --- ## Page: https://react-spectrum.adobe.com/react-aria/examples/image-grid.html React AriaExamples # Image Grid An async-loaded image gallery with selectable items built with the ListBox component, and styled with Tailwind CSS. ## Example# * * * Antonio Verdín Joel Timothy Slava Jamm Milad Fakurian Danny Greenberg Yichen Wang Pawel Czerwinski Valeria Kodra Ingmar Artiom Vallat Hosein Sediqi Quan Jing Chris Weiher Jean Carlo Emer Wenhao Ruan Cash Macanaya Karsten Winegeart Karsten Winegeart Slava Jamm Europeana Hen Kaznelson Sébastien Lavalaye Zhen Yao Jeff James Zhen Yao import {ListBox, ListBoxItem, ProgressBar, Text} from 'react-aria-components'; import {useAsyncList} from 'react-stately'; import CheckCircleIcon from '@spectrum-icons/workflow/CheckmarkCircle'; type Item = { user: { name: string }; urls: { regular: string }; alt_description: string; }; function ImageGridExample() { let list = useAsyncList<Item, number>({ async load({ signal, cursor }) { let page = cursor || 1; let res = await fetch( `https://api.unsplash.com/photos?page=${page}&per_page=25&client_id=AJuU-FPh11hn7RuumUllp4ppT8kgiLS7LtOHp_sp4nc`, { signal } ); let items = await res.json(); return { items, cursor: page + 1 }; } }); let renderEmptyState = () => { if (list.isLoading) { return <ProgressCircle />; } }; return ( <div className="bg-linear-to-r from-sky-500 to-teal-500 p-2 sm:p-8 rounded-lg flex justify-center"> <ListBox aria-label="Images" items={list.items} selectionMode="multiple" layout="grid" renderEmptyState={renderEmptyState} className="overflow-auto outline-hidden bg-white rounded-lg shadow-sm p-2 h-[350px] w-full max-w-[372px] grid grid-cols-3 gap-3 empty:flex" > {(item) => ( <ListBoxItem textValue={item.user.name} className="relative rounded-sm outline-hidden group cursor-default" > <img src={item.urls.regular} alt={item.alt_description} className="h-[80px] w-full object-cover rounded-sm group-selected:ring-2 group-focus-visible:ring-4 group-selected:group-focus-visible:ring-4 ring-offset-2 ring-sky-600" /> <Text slot="label" className="text-[11px] text-gray-700 font-semibold overflow-hidden text-ellipsis whitespace-nowrap max-w-full block mt-1" > {item.user.name} </Text> <div className="absolute top-2 left-2 text-sky-800 rounded-full leading-0 bg-white border border-white border-solid hidden group-selected:block"> <CheckCircleIcon size="S" /> </div> </ListBoxItem> )} </ListBox> </div> ); } function ProgressCircle() { return ( <ProgressBar aria-label="Loading…" isIndeterminate className="flex items-center justify-center w-full" > <svg className="animate-spin h-5 w-5 text-sky-800" fill="none" viewBox="0 0 24 24" > <circle className="opacity-25 stroke-current stroke-[4px]" cx="12" cy="12" r="10" /> <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" > </path> </svg> </ProgressBar> ); } import { ListBox, ListBoxItem, ProgressBar, Text } from 'react-aria-components'; import {useAsyncList} from 'react-stately'; import CheckCircleIcon from '@spectrum-icons/workflow/CheckmarkCircle'; type Item = { user: { name: string }; urls: { regular: string }; alt_description: string; }; function ImageGridExample() { let list = useAsyncList<Item, number>({ async load({ signal, cursor }) { let page = cursor || 1; let res = await fetch( `https://api.unsplash.com/photos?page=${page}&per_page=25&client_id=AJuU-FPh11hn7RuumUllp4ppT8kgiLS7LtOHp_sp4nc`, { signal } ); let items = await res.json(); return { items, cursor: page + 1 }; } }); let renderEmptyState = () => { if (list.isLoading) { return <ProgressCircle />; } }; return ( <div className="bg-linear-to-r from-sky-500 to-teal-500 p-2 sm:p-8 rounded-lg flex justify-center"> <ListBox aria-label="Images" items={list.items} selectionMode="multiple" layout="grid" renderEmptyState={renderEmptyState} className="overflow-auto outline-hidden bg-white rounded-lg shadow-sm p-2 h-[350px] w-full max-w-[372px] grid grid-cols-3 gap-3 empty:flex" > {(item) => ( <ListBoxItem textValue={item.user.name} className="relative rounded-sm outline-hidden group cursor-default" > <img src={item.urls.regular} alt={item.alt_description} className="h-[80px] w-full object-cover rounded-sm group-selected:ring-2 group-focus-visible:ring-4 group-selected:group-focus-visible:ring-4 ring-offset-2 ring-sky-600" /> <Text slot="label" className="text-[11px] text-gray-700 font-semibold overflow-hidden text-ellipsis whitespace-nowrap max-w-full block mt-1" > {item.user.name} </Text> <div className="absolute top-2 left-2 text-sky-800 rounded-full leading-0 bg-white border border-white border-solid hidden group-selected:block"> <CheckCircleIcon size="S" /> </div> </ListBoxItem> )} </ListBox> </div> ); } function ProgressCircle() { return ( <ProgressBar aria-label="Loading…" isIndeterminate className="flex items-center justify-center w-full" > <svg className="animate-spin h-5 w-5 text-sky-800" fill="none" viewBox="0 0 24 24" > <circle className="opacity-25 stroke-current stroke-[4px]" cx="12" cy="12" r="10" /> <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" > </path> </svg> </ProgressBar> ); } import { ListBox, ListBoxItem, ProgressBar, Text } from 'react-aria-components'; import {useAsyncList} from 'react-stately'; import CheckCircleIcon from '@spectrum-icons/workflow/CheckmarkCircle'; type Item = { user: { name: string }; urls: { regular: string; }; alt_description: string; }; function ImageGridExample() { let list = useAsyncList< Item, number >({ async load( { signal, cursor } ) { let page = cursor || 1; let res = await fetch( `https://api.unsplash.com/photos?page=${page}&per_page=25&client_id=AJuU-FPh11hn7RuumUllp4ppT8kgiLS7LtOHp_sp4nc`, { signal } ); let items = await res .json(); return { items, cursor: page + 1 }; } }); let renderEmptyState = () => { if ( list.isLoading ) { return ( <ProgressCircle /> ); } }; return ( <div className="bg-linear-to-r from-sky-500 to-teal-500 p-2 sm:p-8 rounded-lg flex justify-center"> <ListBox aria-label="Images" items={list .items} selectionMode="multiple" layout="grid" renderEmptyState={renderEmptyState} className="overflow-auto outline-hidden bg-white rounded-lg shadow-sm p-2 h-[350px] w-full max-w-[372px] grid grid-cols-3 gap-3 empty:flex" > {(item) => ( <ListBoxItem textValue={item .user.name} className="relative rounded-sm outline-hidden group cursor-default" > <img src={item .urls .regular} alt={item .alt_description} className="h-[80px] w-full object-cover rounded-sm group-selected:ring-2 group-focus-visible:ring-4 group-selected:group-focus-visible:ring-4 ring-offset-2 ring-sky-600" /> <Text slot="label" className="text-[11px] text-gray-700 font-semibold overflow-hidden text-ellipsis whitespace-nowrap max-w-full block mt-1" > {item.user .name} </Text> <div className="absolute top-2 left-2 text-sky-800 rounded-full leading-0 bg-white border border-white border-solid hidden group-selected:block"> <CheckCircleIcon size="S" /> </div> </ListBoxItem> )} </ListBox> </div> ); } function ProgressCircle() { return ( <ProgressBar aria-label="Loading…" isIndeterminate className="flex items-center justify-center w-full" > <svg className="animate-spin h-5 w-5 text-sky-800" fill="none" viewBox="0 0 24 24" > <circle className="opacity-25 stroke-current stroke-[4px]" cx="12" cy="12" r="10" /> <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" > </path> </svg> </ProgressBar> ); } ### Tailwind config# This example uses the tailwindcss-react-aria-components plugin. When using Tailwind v4, add it to your CSS: @import "tailwindcss"; @plugin "tailwindcss-react-aria-components"; @import "tailwindcss"; @plugin "tailwindcss-react-aria-components"; @import "tailwindcss"; @plugin "tailwindcss-react-aria-components"; Tailwind v3 When using Tailwind v3, add the plugin to your `tailwind.config.js` instead: module.exports = { // ... plugins: [ require('tailwindcss-react-aria-components') ] }; module.exports = { // ... plugins: [ require('tailwindcss-react-aria-components') ] }; module.exports = { // ... plugins: [ require( 'tailwindcss-react-aria-components' ) ] }; **Note**: When using Tailwind v3, install `tailwindcss-react-aria-components` version 1.x instead of 2.x. ## Components# * * * ListBox A listbox displays a list of options, and allows a user to select one or more of them. ProgressBar A progress bar shows progress of an operation over time. --- ## Page: https://react-spectrum.adobe.com/react-aria/examples/ios-list.html React AriaExamples # iOS List View A re-creation of the iOS List View built with React Aria Components, Framer Motion, and Tailwind CSS with support for swipe gestures, layout animations, and multiple selection mode. ## Example# * * * DeleteEdit Emma Johnson 9:40 AM Meeting Reminder: Project Kickoff Dear Devon, This is a friendly reminder of the upcoming project kickoff meeting scheduled for tomorrow at 9am. The meeting will be held in \[location\]. It's essential that all team members attend to ensure a successful start to the project. Please come prepared with any necessary materials or information relevant to the project. If you have any questions or need further clarification, don't hesitate to reach out to me. Looking forward to seeing you at the meeting. Best regards, Emma Delete support@company.com 8:23 AM Important Account Update Dear Devon, We hope this email finds you well. We are writing to inform you about an important update regarding your account with us. As part of our ongoing efforts to enhance security, we have implemented a new two-factor authentication process. To ensure continued access to your account, please follow the instructions provided in the attached document to set up the two-factor authentication feature. If you have any questions or need assistance, please don't hesitate to contact our support team. Thank you for your cooperation. Best regards, The \[Company\] Team Delete Liam Thompson Yesterday Promotion Announcement Dear Devon, We are pleased to inform you that based on your exceptional performance, dedication, and contributions to the company, you have been promoted to the position of \[new position\]. This promotion is a recognition of your hard work and the value you bring to our organization. Please accept our heartfelt congratulations on this well-deserved achievement. We believe that you will excel in your new role and contribute to the continued success of our team. If you have any questions or need any support during this transition, please don't hesitate to contact the HR department. Best regards, The HR Team Delete events@company.com Yesterday Invitation to Exclusive Networking Event Dear Devon, You are cordially invited to our upcoming exclusive networking event, where industry leaders, professionals, and enthusiasts gather to exchange ideas and forge valuable connections. This event will take place on \[date\] at \[venue\], starting at \[time\]. Please RSVP by \[RSVP date\] to secure your spot. We anticipate a high demand for attendance, so we encourage you to respond promptly. We look forward to welcoming you to this exciting event! Best regards, The \[Company\] Events Team Delete sales@company.com Friday Thank You for Your Recent Purchase Dear Devon, Thank you for your recent purchase from our online store. We appreciate your business and are delighted to let you know that your order has been successfully processed and is now being prepared for shipment. You will receive a confirmation email with tracking details as soon as your package is dispatched. If you have any questions regarding your order or need further assistance, please don't hesitate to reach out to our customer support team. Once again, thank you for choosing us as your preferred shopping destination. Best regards, The \[Company\] Team Delete Jane Doe Friday New Project Proposal Hi Devon, I've attached a new project proposal for your review. Please let me know what you think. Thanks, Jane Delete Susan Smith Friday Status Update Hi Devon, I'm just sending a quick status update on the project we're working on together. I'm on track to meet my deadlines, and I'll keep you updated on my progress. Thanks, Susan Delete Michael Jones Thursday Question about the presentation Hi Devon, I had a question about the presentation you gave last week. I was wondering if you could send me the slides so I can review them in more detail. Thanks, Michael Delete Customer Service Thursday Order Confirmation Hi Devon, We just wanted to confirm that your order has been shipped. Your order number is 1234567890, and it should arrive at your home address within 2-3 business days. Thanks for your purchase! Customer Service Delete Your Bank Wednesday Account Statement Hi Devon, We're writing to you today to provide you with your monthly account statement. As you can see, your account balance is currently $1,000.00. Please let us know if you have any questions. Thanks, Your Bank Delete hr@company2.com Tuesday Employee Benefits Update Dear Devon, We wanted to inform you about the recent updates to our employee benefits package. We have enhanced the healthcare coverage options and added additional wellness programs to support your well-being. Please review the attached document for detailed information on the updated benefits. If you have any questions or need further assistance, feel free to contact the HR department. Best regards, The HR Team Delete import {Button, GridList, GridListItem} from 'react-aria-components'; import type {Selection, SelectionMode} from 'react-aria-components'; import {animate, AnimatePresence, motion, useIsPresent, useMotionTemplate, useMotionValue, useMotionValueEvent} from 'framer-motion'; import {useRef, useState} from 'react'; import type {CSSProperties} from 'react'; const MotionItem = motion(GridListItem); const inertiaTransition = { type: 'inertia' as const, bounceStiffness: 300, bounceDamping: 40, timeConstant: 300 }; function SwipableList() { let [items, setItems] = useState(messages.emails); let [selectedKeys, setSelectedKeys] = useState<Selection>(new Set()); let [selectionMode, setSelectionMode] = useState<SelectionMode>('none'); let onDelete = () => { setItems( items.filter((i) => selectedKeys !== 'all' && !selectedKeys.has(i.id)) ); setSelectedKeys(new Set()); setSelectionMode('none'); }; return ( <div className="flex flex-col h-full max-h-[500px] sm:w-[400px] -mx-[14px] sm:mx-0"> {/* Toolbar */} <div className="flex pb-4 justify-between"> <Button className="text-blue-600 text-lg outline-hidden bg-transparent border-none transition pressed:text-blue-700 focus-visible:ring-3 disabled:text-gray-400" style={{ opacity: selectionMode === 'none' ? 0 : 1 }} isDisabled={selectedKeys !== 'all' && selectedKeys.size === 0} onPress={onDelete} > Delete </Button> <Button className="text-blue-600 text-lg outline-hidden bg-transparent border-none transition pressed:text-blue-700 focus-visible:ring-3" onPress={() => { setSelectionMode((m) => (m === 'none' ? 'multiple' : 'none')); setSelectedKeys(new Set()); }} > {selectionMode === 'none' ? 'Edit' : 'Cancel'} </Button> </div> <GridList className="relative flex-1 overflow-auto" aria-label="Inbox" onAction={selectionMode === 'none' ? () => {} : undefined} selectionMode={selectionMode} selectedKeys={selectedKeys} onSelectionChange={setSelectedKeys} > <AnimatePresence> {items.map((item) => ( <ListItem key={item.id} id={item.id} textValue={[item.sender, item.date, item.subject, item.message] .join('\n')} onRemove={() => setItems(items.filter((i) => i !== item))} > <div className="flex flex-col text-md cursor-default"> <div className="flex justify-between"> <p className="font-bold text-lg m-0">{item.sender}</p> <p className="text-gray-500 m-0">{item.date}</p> </div> <p className="m-0">{item.subject}</p> <p className="line-clamp-2 text-gray-500 dark:text-gray-400 m-0"> {item.message} </p> </div> </ListItem> ))} </AnimatePresence> </GridList> </div> ); } function ListItem({ id, children, textValue, onRemove }) { let ref = useRef(null); let x = useMotionValue(0); let isPresent = useIsPresent(); let xPx = useMotionTemplate`${x}px`; // Align the text in the remove button to the left if the // user has swiped at least 80% of the width. let [align, setAlign] = useState('end'); useMotionValueEvent(x, 'change', (x) => { let a = x < -ref.current?.offsetWidth * 0.8 ? 'start' : 'end'; setAlign(a); }); return ( <MotionItem id={id} textValue={textValue} className="outline-hidden group relative overflow-clip border-t border-0 border-solid last:border-b border-gray-200 dark:border-gray-800 pressed:bg-gray-200 dark:pressed:bg-gray-800 selected:bg-gray-200 dark:selected:bg-gray-800 focus-visible:outline focus-visible:outline-blue-600 focus-visible:-outline-offset-2" layout transition={{ duration: 0.25 }} exit={{ opacity: 0 }} // Take item out of the flow if it is being removed. style={{ position: isPresent ? 'relative' : 'absolute' }} > {/* @ts-ignore - Framer Motion's types don't handle functions properly. */} {({ selectionMode, isSelected }) => ( // Content of the item can be swiped to reveal the delete button, or fully swiped to delete. <motion.div ref={ref} style={{ x, '--x': xPx } as CSSProperties} className="flex items-center" drag={selectionMode === 'none' ? 'x' : undefined} dragConstraints={{ right: 0 }} onDragEnd={(e, { offset }) => { // If the user dragged past 80% of the width, remove the item // otherwise animate back to the nearest snap point. let v = offset.x > -20 ? 0 : -100; if (x.get() < -ref.current.offsetWidth * 0.8) { v = -ref.current.offsetWidth; onRemove(); } animate(x, v, { ...inertiaTransition, min: v, max: v }); }} onDragStart={() => { // Cancel react-aria press event when dragging starts. document.dispatchEvent(new PointerEvent('pointercancel')); }} > {selectionMode === 'multiple' && ( <SelectionCheckmark isSelected={isSelected} /> )} <motion.div layout layoutDependency={selectionMode} transition={{ duration: 0.25 }} className="relative flex items-center px-4 py-2 z-10" > {children} </motion.div> {selectionMode === 'none' && ( <Button className="bg-red-600 pressed:bg-red-700 cursor-default text-lg outline-hidden border-none transition-colors text-white flex items-center absolute top-0 left-[100%] py-2 h-full z-0 isolate focus-visible:outline focus-visible:outline-blue-600 focus-visible:-outline-offset-2" style={{ // Calculate the size of the button based on the drag position, // which is stored in a CSS variable above. width: 'max(100px, calc(-1 * var(--x)))', justifyContent: align }} onPress={onRemove} // Move the button into view when it is focused with the keyboard // (e.g. via the arrow keys). onFocus={() => x.set(-100)} onBlur={() => x.set(0)} > <motion.span initial={false} className="px-4" animate={{ // Whenever the alignment changes, perform a keyframe animation // between the previous position and new position. This is done // by calculating a transform for the previous alignment and // animating it back to zero. transform: align === 'start' ? ['translateX(calc(-100% - var(--x)))', 'translateX(0)'] : ['translateX(calc(100% + var(--x)))', 'translateX(0)'] }} > Delete </motion.span> </Button> )} </motion.div> )} </MotionItem> ); } function SelectionCheckmark({ isSelected }) { return ( <motion.svg aria-hidden="true" viewBox="0 0 24 24" fill="currentColor" className="w-6 h-6 shrink-0 ml-4" initial={{ x: -40 }} animate={{ x: 0 }} transition={{ duration: 0.25 }} > {!isSelected && ( <circle r={9} cx={12} cy={12} stroke="currentColor" fill="none" strokeWidth={1} className="text-gray-400" /> )} {isSelected && ( <path className="text-blue-600" fillRule="evenodd" d="M2.25 12c0-5.385 4.365-9.75 9.75-9.75s9.75 4.365 9.75 9.75-4.365 9.75-9.75 9.75S2.25 17.385 2.25 12zm13.36-1.814a.75.75 0 10-1.22-.872l-3.236 4.53L9.53 12.22a.75.75 0 00-1.06 1.06l2.25 2.25a.75.75 0 001.14-.094l3.75-5.25z" clipRule="evenodd" /> )} </motion.svg> ); } import { Button, GridList, GridListItem } from 'react-aria-components'; import type { Selection, SelectionMode } from 'react-aria-components'; import { animate, AnimatePresence, motion, useIsPresent, useMotionTemplate, useMotionValue, useMotionValueEvent } from 'framer-motion'; import {useRef, useState} from 'react'; import type {CSSProperties} from 'react'; const MotionItem = motion(GridListItem); const inertiaTransition = { type: 'inertia' as const, bounceStiffness: 300, bounceDamping: 40, timeConstant: 300 }; function SwipableList() { let [items, setItems] = useState(messages.emails); let [selectedKeys, setSelectedKeys] = useState<Selection>( new Set() ); let [selectionMode, setSelectionMode] = useState< SelectionMode >('none'); let onDelete = () => { setItems( items.filter((i) => selectedKeys !== 'all' && !selectedKeys.has(i.id) ) ); setSelectedKeys(new Set()); setSelectionMode('none'); }; return ( <div className="flex flex-col h-full max-h-[500px] sm:w-[400px] -mx-[14px] sm:mx-0"> {/* Toolbar */} <div className="flex pb-4 justify-between"> <Button className="text-blue-600 text-lg outline-hidden bg-transparent border-none transition pressed:text-blue-700 focus-visible:ring-3 disabled:text-gray-400" style={{ opacity: selectionMode === 'none' ? 0 : 1 }} isDisabled={selectedKeys !== 'all' && selectedKeys.size === 0} onPress={onDelete} > Delete </Button> <Button className="text-blue-600 text-lg outline-hidden bg-transparent border-none transition pressed:text-blue-700 focus-visible:ring-3" onPress={() => { setSelectionMode(( m ) => (m === 'none' ? 'multiple' : 'none')); setSelectedKeys(new Set()); }} > {selectionMode === 'none' ? 'Edit' : 'Cancel'} </Button> </div> <GridList className="relative flex-1 overflow-auto" aria-label="Inbox" onAction={selectionMode === 'none' ? () => {} : undefined} selectionMode={selectionMode} selectedKeys={selectedKeys} onSelectionChange={setSelectedKeys} > <AnimatePresence> {items.map((item) => ( <ListItem key={item.id} id={item.id} textValue={[ item.sender, item.date, item.subject, item.message ].join('\n')} onRemove={() => setItems(items.filter((i) => i !== item))} > <div className="flex flex-col text-md cursor-default"> <div className="flex justify-between"> <p className="font-bold text-lg m-0"> {item.sender} </p> <p className="text-gray-500 m-0"> {item.date} </p> </div> <p className="m-0">{item.subject}</p> <p className="line-clamp-2 text-gray-500 dark:text-gray-400 m-0"> {item.message} </p> </div> </ListItem> ))} </AnimatePresence> </GridList> </div> ); } function ListItem({ id, children, textValue, onRemove }) { let ref = useRef(null); let x = useMotionValue(0); let isPresent = useIsPresent(); let xPx = useMotionTemplate`${x}px`; // Align the text in the remove button to the left if the // user has swiped at least 80% of the width. let [align, setAlign] = useState('end'); useMotionValueEvent(x, 'change', (x) => { let a = x < -ref.current?.offsetWidth * 0.8 ? 'start' : 'end'; setAlign(a); }); return ( <MotionItem id={id} textValue={textValue} className="outline-hidden group relative overflow-clip border-t border-0 border-solid last:border-b border-gray-200 dark:border-gray-800 pressed:bg-gray-200 dark:pressed:bg-gray-800 selected:bg-gray-200 dark:selected:bg-gray-800 focus-visible:outline focus-visible:outline-blue-600 focus-visible:-outline-offset-2" layout transition={{ duration: 0.25 }} exit={{ opacity: 0 }} // Take item out of the flow if it is being removed. style={{ position: isPresent ? 'relative' : 'absolute' }} > {/* @ts-ignore - Framer Motion's types don't handle functions properly. */} {({ selectionMode, isSelected }) => ( // Content of the item can be swiped to reveal the delete button, or fully swiped to delete. <motion.div ref={ref} style={{ x, '--x': xPx } as CSSProperties} className="flex items-center" drag={selectionMode === 'none' ? 'x' : undefined} dragConstraints={{ right: 0 }} onDragEnd={(e, { offset }) => { // If the user dragged past 80% of the width, remove the item // otherwise animate back to the nearest snap point. let v = offset.x > -20 ? 0 : -100; if (x.get() < -ref.current.offsetWidth * 0.8) { v = -ref.current.offsetWidth; onRemove(); } animate(x, v, { ...inertiaTransition, min: v, max: v }); }} onDragStart={() => { // Cancel react-aria press event when dragging starts. document.dispatchEvent( new PointerEvent('pointercancel') ); }} > {selectionMode === 'multiple' && ( <SelectionCheckmark isSelected={isSelected} /> )} <motion.div layout layoutDependency={selectionMode} transition={{ duration: 0.25 }} className="relative flex items-center px-4 py-2 z-10" > {children} </motion.div> {selectionMode === 'none' && ( <Button className="bg-red-600 pressed:bg-red-700 cursor-default text-lg outline-hidden border-none transition-colors text-white flex items-center absolute top-0 left-[100%] py-2 h-full z-0 isolate focus-visible:outline focus-visible:outline-blue-600 focus-visible:-outline-offset-2" style={{ // Calculate the size of the button based on the drag position, // which is stored in a CSS variable above. width: 'max(100px, calc(-1 * var(--x)))', justifyContent: align }} onPress={onRemove} // Move the button into view when it is focused with the keyboard // (e.g. via the arrow keys). onFocus={() => x.set(-100)} onBlur={() => x.set(0)} > <motion.span initial={false} className="px-4" animate={{ // Whenever the alignment changes, perform a keyframe animation // between the previous position and new position. This is done // by calculating a transform for the previous alignment and // animating it back to zero. transform: align === 'start' ? [ 'translateX(calc(-100% - var(--x)))', 'translateX(0)' ] : [ 'translateX(calc(100% + var(--x)))', 'translateX(0)' ] }} > Delete </motion.span> </Button> )} </motion.div> )} </MotionItem> ); } function SelectionCheckmark({ isSelected }) { return ( <motion.svg aria-hidden="true" viewBox="0 0 24 24" fill="currentColor" className="w-6 h-6 shrink-0 ml-4" initial={{ x: -40 }} animate={{ x: 0 }} transition={{ duration: 0.25 }} > {!isSelected && ( <circle r={9} cx={12} cy={12} stroke="currentColor" fill="none" strokeWidth={1} className="text-gray-400" /> )} {isSelected && ( <path className="text-blue-600" fillRule="evenodd" d="M2.25 12c0-5.385 4.365-9.75 9.75-9.75s9.75 4.365 9.75 9.75-4.365 9.75-9.75 9.75S2.25 17.385 2.25 12zm13.36-1.814a.75.75 0 10-1.22-.872l-3.236 4.53L9.53 12.22a.75.75 0 00-1.06 1.06l2.25 2.25a.75.75 0 001.14-.094l3.75-5.25z" clipRule="evenodd" /> )} </motion.svg> ); } import { Button, GridList, GridListItem } from 'react-aria-components'; import type { Selection, SelectionMode } from 'react-aria-components'; import { animate, AnimatePresence, motion, useIsPresent, useMotionTemplate, useMotionValue, useMotionValueEvent } from 'framer-motion'; import { useRef, useState } from 'react'; import type {CSSProperties} from 'react'; const MotionItem = motion(GridListItem); const inertiaTransition = { type: 'inertia' as const, bounceStiffness: 300, bounceDamping: 40, timeConstant: 300 }; function SwipableList() { let [items, setItems] = useState( messages.emails ); let [ selectedKeys, setSelectedKeys ] = useState< Selection >(new Set()); let [ selectionMode, setSelectionMode ] = useState< SelectionMode >('none'); let onDelete = () => { setItems( items.filter((i) => selectedKeys !== 'all' && !selectedKeys .has(i.id) ) ); setSelectedKeys( new Set() ); setSelectionMode( 'none' ); }; return ( <div className="flex flex-col h-full max-h-[500px] sm:w-[400px] -mx-[14px] sm:mx-0"> {/* Toolbar */} <div className="flex pb-4 justify-between"> <Button className="text-blue-600 text-lg outline-hidden bg-transparent border-none transition pressed:text-blue-700 focus-visible:ring-3 disabled:text-gray-400" style={{ opacity: selectionMode === 'none' ? 0 : 1 }} isDisabled={selectedKeys !== 'all' && selectedKeys .size === 0} onPress={onDelete} > Delete </Button> <Button className="text-blue-600 text-lg outline-hidden bg-transparent border-none transition pressed:text-blue-700 focus-visible:ring-3" onPress={() => { setSelectionMode( ( m ) => (m === 'none' ? 'multiple' : 'none') ); setSelectedKeys( new Set() ); }} > {selectionMode === 'none' ? 'Edit' : 'Cancel'} </Button> </div> <GridList className="relative flex-1 overflow-auto" aria-label="Inbox" onAction={selectionMode === 'none' ? () => {} : undefined} selectionMode={selectionMode} selectedKeys={selectedKeys} onSelectionChange={setSelectedKeys} > <AnimatePresence> {items.map(( item ) => ( <ListItem key={item .id} id={item .id} textValue={[ item .sender, item .date, item .subject, item .message ].join( '\n' )} onRemove={() => setItems( items .filter( ( i ) => i !== item ) )} > <div className="flex flex-col text-md cursor-default"> <div className="flex justify-between"> <p className="font-bold text-lg m-0"> {item .sender} </p> <p className="text-gray-500 m-0"> {item .date} </p> </div> <p className="m-0"> {item .subject} </p> <p className="line-clamp-2 text-gray-500 dark:text-gray-400 m-0"> {item .message} </p> </div> </ListItem> ))} </AnimatePresence> </GridList> </div> ); } function ListItem( { id, children, textValue, onRemove } ) { let ref = useRef(null); let x = useMotionValue( 0 ); let isPresent = useIsPresent(); let xPx = useMotionTemplate`${x}px`; // Align the text in the remove button to the left if the // user has swiped at least 80% of the width. let [align, setAlign] = useState('end'); useMotionValueEvent( x, 'change', (x) => { let a = x < -ref.current ?.offsetWidth * 0.8 ? 'start' : 'end'; setAlign(a); } ); return ( <MotionItem id={id} textValue={textValue} className="outline-hidden group relative overflow-clip border-t border-0 border-solid last:border-b border-gray-200 dark:border-gray-800 pressed:bg-gray-200 dark:pressed:bg-gray-800 selected:bg-gray-200 dark:selected:bg-gray-800 focus-visible:outline focus-visible:outline-blue-600 focus-visible:-outline-offset-2" layout transition={{ duration: 0.25 }} exit={{ opacity: 0 }} // Take item out of the flow if it is being removed. style={{ position: isPresent ? 'relative' : 'absolute' }} > {/* @ts-ignore - Framer Motion's types don't handle functions properly. */} {( { selectionMode, isSelected } ) => ( // Content of the item can be swiped to reveal the delete button, or fully swiped to delete. <motion.div ref={ref} style={{ x, '--x': xPx } as CSSProperties} className="flex items-center" drag={selectionMode === 'none' ? 'x' : undefined} dragConstraints={{ right: 0 }} onDragEnd={( e, { offset } ) => { // If the user dragged past 80% of the width, remove the item // otherwise animate back to the nearest snap point. let v = offset.x > -20 ? 0 : -100; if ( x.get() < -ref .current .offsetWidth * 0.8 ) { v = -ref .current .offsetWidth; onRemove(); } animate( x, v, { ...inertiaTransition, min: v, max: v } ); }} onDragStart={() => { // Cancel react-aria press event when dragging starts. document .dispatchEvent( new PointerEvent( 'pointercancel' ) ); }} > {selectionMode === 'multiple' && ( <SelectionCheckmark isSelected={isSelected} /> )} <motion.div layout layoutDependency={selectionMode} transition={{ duration: 0.25 }} className="relative flex items-center px-4 py-2 z-10" > {children} </motion.div> {selectionMode === 'none' && ( <Button className="bg-red-600 pressed:bg-red-700 cursor-default text-lg outline-hidden border-none transition-colors text-white flex items-center absolute top-0 left-[100%] py-2 h-full z-0 isolate focus-visible:outline focus-visible:outline-blue-600 focus-visible:-outline-offset-2" style={{ // Calculate the size of the button based on the drag position, // which is stored in a CSS variable above. width: 'max(100px, calc(-1 * var(--x)))', justifyContent: align }} onPress={onRemove} // Move the button into view when it is focused with the keyboard // (e.g. via the arrow keys). onFocus={() => x.set( -100 )} onBlur={() => x.set(0)} > <motion.span initial={false} className="px-4" animate={{ // Whenever the alignment changes, perform a keyframe animation // between the previous position and new position. This is done // by calculating a transform for the previous alignment and // animating it back to zero. transform: align === 'start' ? [ 'translateX(calc(-100% - var(--x)))', 'translateX(0)' ] : [ 'translateX(calc(100% + var(--x)))', 'translateX(0)' ] }} > Delete </motion.span> </Button> )} </motion.div> )} </MotionItem> ); } function SelectionCheckmark( { isSelected } ) { return ( <motion.svg aria-hidden="true" viewBox="0 0 24 24" fill="currentColor" className="w-6 h-6 shrink-0 ml-4" initial={{ x: -40 }} animate={{ x: 0 }} transition={{ duration: 0.25 }} > {!isSelected && ( <circle r={9} cx={12} cy={12} stroke="currentColor" fill="none" strokeWidth={1} className="text-gray-400" /> )} {isSelected && ( <path className="text-blue-600" fillRule="evenodd" d="M2.25 12c0-5.385 4.365-9.75 9.75-9.75s9.75 4.365 9.75 9.75-4.365 9.75-9.75 9.75S2.25 17.385 2.25 12zm13.36-1.814a.75.75 0 10-1.22-.872l-3.236 4.53L9.53 12.22a.75.75 0 00-1.06 1.06l2.25 2.25a.75.75 0 001.14-.094l3.75-5.25z" clipRule="evenodd" /> )} </motion.svg> ); } ### Tailwind config# This example uses the tailwindcss-react-aria-components plugin. When using Tailwind v4, add it to your CSS: @import "tailwindcss"; @plugin "tailwindcss-react-aria-components"; @import "tailwindcss"; @plugin "tailwindcss-react-aria-components"; @import "tailwindcss"; @plugin "tailwindcss-react-aria-components"; Tailwind v3 When using Tailwind v3, add the plugin to your `tailwind.config.js` instead: module.exports = { // ... plugins: [ require('tailwindcss-react-aria-components') ] }; module.exports = { // ... plugins: [ require('tailwindcss-react-aria-components') ] }; module.exports = { // ... plugins: [ require( 'tailwindcss-react-aria-components' ) ] }; **Note**: When using Tailwind v3, install `tailwindcss-react-aria-components` version 1.x instead of 2.x. ## Components# * * * GridList A grid list displays a list of interactive items, with keyboard navigation, row selection, and actions. Button A button allows a user to perform an action. --- ## Page: https://react-spectrum.adobe.com/react-aria/examples/loading-progress.html React AriaExamples # Loading ProgressBar A loading ProgressBar styled with Tailwind CSS. ## Example# * * * Loading…30% import {Label, ProgressBar} from 'react-aria-components'; <div className="bg-linear-to-r from-blue-600 to-purple-600 p-12 rounded-lg flex justify-center"> <ProgressBar value={30} className="flex flex-col gap-3 w-56 text-white"> {({ percentage, valueText }) => ( <> <div className="flex"> <Label className="flex-1">Loading…</Label> <span>{valueText}</span> </div> <div className="h-2 top-[50%] transform translate-y-[-50%] w-full rounded-full bg-white/40"> <div className="absolute h-2 top-[50%] transform translate-y-[-50%] rounded-full bg-white" style={{ width: percentage + '%' }} /> </div> </> )} </ProgressBar> </div> import {Label, ProgressBar} from 'react-aria-components'; <div className="bg-linear-to-r from-blue-600 to-purple-600 p-12 rounded-lg flex justify-center"> <ProgressBar value={30} className="flex flex-col gap-3 w-56 text-white" > {({ percentage, valueText }) => ( <> <div className="flex"> <Label className="flex-1">Loading…</Label> <span>{valueText}</span> </div> <div className="h-2 top-[50%] transform translate-y-[-50%] w-full rounded-full bg-white/40"> <div className="absolute h-2 top-[50%] transform translate-y-[-50%] rounded-full bg-white" style={{ width: percentage + '%' }} /> </div> </> )} </ProgressBar> </div> import { Label, ProgressBar } from 'react-aria-components'; <div className="bg-linear-to-r from-blue-600 to-purple-600 p-12 rounded-lg flex justify-center"> <ProgressBar value={30} className="flex flex-col gap-3 w-56 text-white" > {( { percentage, valueText } ) => ( <> <div className="flex"> <Label className="flex-1"> Loading… </Label> <span> {valueText} </span> </div> <div className="h-2 top-[50%] transform translate-y-[-50%] w-full rounded-full bg-white/40"> <div className="absolute h-2 top-[50%] transform translate-y-[-50%] rounded-full bg-white" style={{ width: percentage + '%' }} /> </div> </> )} </ProgressBar> </div> ### Tailwind config# This example uses the tailwindcss-react-aria-components plugin. When using Tailwind v4, add it to your CSS: @import "tailwindcss"; @plugin "tailwindcss-react-aria-components"; @import "tailwindcss"; @plugin "tailwindcss-react-aria-components"; @import "tailwindcss"; @plugin "tailwindcss-react-aria-components"; Tailwind v3 When using Tailwind v3, add the plugin to your `tailwind.config.js` instead: module.exports = { // ... plugins: [ require('tailwindcss-react-aria-components') ] }; module.exports = { // ... plugins: [ require('tailwindcss-react-aria-components') ] }; module.exports = { // ... plugins: [ require( 'tailwindcss-react-aria-components' ) ] }; **Note**: When using Tailwind v3, install `tailwindcss-react-aria-components` version 1.x instead of 2.x. ## Components# * * * ProgressBar A progress bar shows progress of an operation over time. --- ## Page: https://react-spectrum.adobe.com/react-aria/examples/notifications-popover.html React AriaExamples # Notifications Popover A notifications Popover styled with Tailwind CSS. ## Example# * * * import {Button, Dialog, DialogTrigger, Link, OverlayArrow, Popover} from 'react-aria-components'; import type {PopoverProps} from 'react-aria-components'; import BellIcon from '@spectrum-icons/workflow/Bell'; import ChatIcon from '@spectrum-icons/workflow/Chat'; function PopoverExample() { return ( <div className="bg-linear-to-r from-orange-400 to-pink-600 p-8 rounded-lg sm:h-[300px] flex items-start justify-center"> <DialogTrigger> <Button aria-label="Notifications" className="inline-flex items-center justify-center rounded-md bg-black/20 bg-clip-padding border border-white/20 px-3.5 py-2 font-medium text-white hover:bg-black/30 pressed:bg-black/40 transition-colors cursor-default outline-hidden focus-visible:ring-2 focus-visible:ring-white/75" > <BellIcon size="S" /> </Button> <MyPopover> <OverlayArrow> <svg viewBox="0 0 12 12" className="block fill-white group-placement-bottom:rotate-180 w-4 h-4" > <path d="M0 0L6 6L12 0" /> </svg> </OverlayArrow> <Dialog className="p-2 outline-hidden text-gray-700"> <div className="flex flex-col"> <Notification avatar="https://images.unsplash.com/photo-1569913486515-b74bf7751574?ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&ixlib=rb-1.2.1&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80" name="Sonja Balmann" time="2h" text="This looks great! Let's ship it." /> <Notification avatar="https://images.unsplash.com/photo-1494790108377-be9c29b29330?ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&ixlib=rb-1.2.1&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80" name="Maia Pettegree" time="4h" text="Can you add a bit more pizzazz?" /> <Notification avatar="https://images.unsplash.com/photo-1500648767791-00dcc994a43e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2.25&w=256&h=256&q=80" name="Charles Webb" time="1d" text="Here's a first pass. What do you think?" /> </div> </Dialog> </MyPopover> </DialogTrigger> </div> ); } function MyPopover(props: PopoverProps) { return ( <Popover {...props} className={({ isEntering, isExiting }) => ` w-[280px] placement-bottom:mt-2 placement-top:mb-2 group rounded-lg drop-shadow-lg ring-1 ring-black/10 bg-white ${ isEntering ? 'animate-in fade-in placement-bottom:slide-in-from-top-1 placement-top:slide-in-from-bottom-1 ease-out duration-200' : '' } ${ isExiting ? 'animate-out fade-out placement-bottom:slide-out-to-top-1 placement-top:slide-out-to-bottom-1 ease-in duration-150' : '' } `} /> ); } function Notification({ avatar, name, time, text }) { return ( <Link href="#" className="p-2 rounded-lg hover:bg-gray-100 grid grid-cols-[theme(width.5)_1fr_theme(width.4)] gap-x-2 text-[inherit] no-underline outline-hidden focus-visible:ring-2 ring-pink-800" > <img src={avatar} className="rounded-full w-5 h-5 row-span-3" /> <div className="text-gray-800 font-semibold leading-5">{name}</div> <div className="text-gray-400"> <ChatIcon size="XS" /> </div> <div className="text-sm text-gray-500 col-span-2"> Commented {time} ago </div> <p className="text-sm overflow-hidden text-ellipsis line-clamp-2 mt-1 mb-0 col-span-2"> {text} </p> </Link> ); } import { Button, Dialog, DialogTrigger, Link, OverlayArrow, Popover } from 'react-aria-components'; import type {PopoverProps} from 'react-aria-components'; import BellIcon from '@spectrum-icons/workflow/Bell'; import ChatIcon from '@spectrum-icons/workflow/Chat'; function PopoverExample() { return ( <div className="bg-linear-to-r from-orange-400 to-pink-600 p-8 rounded-lg sm:h-[300px] flex items-start justify-center"> <DialogTrigger> <Button aria-label="Notifications" className="inline-flex items-center justify-center rounded-md bg-black/20 bg-clip-padding border border-white/20 px-3.5 py-2 font-medium text-white hover:bg-black/30 pressed:bg-black/40 transition-colors cursor-default outline-hidden focus-visible:ring-2 focus-visible:ring-white/75" > <BellIcon size="S" /> </Button> <MyPopover> <OverlayArrow> <svg viewBox="0 0 12 12" className="block fill-white group-placement-bottom:rotate-180 w-4 h-4" > <path d="M0 0L6 6L12 0" /> </svg> </OverlayArrow> <Dialog className="p-2 outline-hidden text-gray-700"> <div className="flex flex-col"> <Notification avatar="https://images.unsplash.com/photo-1569913486515-b74bf7751574?ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&ixlib=rb-1.2.1&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80" name="Sonja Balmann" time="2h" text="This looks great! Let's ship it." /> <Notification avatar="https://images.unsplash.com/photo-1494790108377-be9c29b29330?ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&ixlib=rb-1.2.1&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80" name="Maia Pettegree" time="4h" text="Can you add a bit more pizzazz?" /> <Notification avatar="https://images.unsplash.com/photo-1500648767791-00dcc994a43e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2.25&w=256&h=256&q=80" name="Charles Webb" time="1d" text="Here's a first pass. What do you think?" /> </div> </Dialog> </MyPopover> </DialogTrigger> </div> ); } function MyPopover(props: PopoverProps) { return ( <Popover {...props} className={({ isEntering, isExiting }) => ` w-[280px] placement-bottom:mt-2 placement-top:mb-2 group rounded-lg drop-shadow-lg ring-1 ring-black/10 bg-white ${ isEntering ? 'animate-in fade-in placement-bottom:slide-in-from-top-1 placement-top:slide-in-from-bottom-1 ease-out duration-200' : '' } ${ isExiting ? 'animate-out fade-out placement-bottom:slide-out-to-top-1 placement-top:slide-out-to-bottom-1 ease-in duration-150' : '' } `} /> ); } function Notification({ avatar, name, time, text }) { return ( <Link href="#" className="p-2 rounded-lg hover:bg-gray-100 grid grid-cols-[theme(width.5)_1fr_theme(width.4)] gap-x-2 text-[inherit] no-underline outline-hidden focus-visible:ring-2 ring-pink-800" > <img src={avatar} className="rounded-full w-5 h-5 row-span-3" /> <div className="text-gray-800 font-semibold leading-5"> {name} </div> <div className="text-gray-400"> <ChatIcon size="XS" /> </div> <div className="text-sm text-gray-500 col-span-2"> Commented {time} ago </div> <p className="text-sm overflow-hidden text-ellipsis line-clamp-2 mt-1 mb-0 col-span-2"> {text} </p> </Link> ); } import { Button, Dialog, DialogTrigger, Link, OverlayArrow, Popover } from 'react-aria-components'; import type {PopoverProps} from 'react-aria-components'; import BellIcon from '@spectrum-icons/workflow/Bell'; import ChatIcon from '@spectrum-icons/workflow/Chat'; function PopoverExample() { return ( <div className="bg-linear-to-r from-orange-400 to-pink-600 p-8 rounded-lg sm:h-[300px] flex items-start justify-center"> <DialogTrigger> <Button aria-label="Notifications" className="inline-flex items-center justify-center rounded-md bg-black/20 bg-clip-padding border border-white/20 px-3.5 py-2 font-medium text-white hover:bg-black/30 pressed:bg-black/40 transition-colors cursor-default outline-hidden focus-visible:ring-2 focus-visible:ring-white/75" > <BellIcon size="S" /> </Button> <MyPopover> <OverlayArrow> <svg viewBox="0 0 12 12" className="block fill-white group-placement-bottom:rotate-180 w-4 h-4" > <path d="M0 0L6 6L12 0" /> </svg> </OverlayArrow> <Dialog className="p-2 outline-hidden text-gray-700"> <div className="flex flex-col"> <Notification avatar="https://images.unsplash.com/photo-1569913486515-b74bf7751574?ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&ixlib=rb-1.2.1&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80" name="Sonja Balmann" time="2h" text="This looks great! Let's ship it." /> <Notification avatar="https://images.unsplash.com/photo-1494790108377-be9c29b29330?ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&ixlib=rb-1.2.1&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80" name="Maia Pettegree" time="4h" text="Can you add a bit more pizzazz?" /> <Notification avatar="https://images.unsplash.com/photo-1500648767791-00dcc994a43e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2.25&w=256&h=256&q=80" name="Charles Webb" time="1d" text="Here's a first pass. What do you think?" /> </div> </Dialog> </MyPopover> </DialogTrigger> </div> ); } function MyPopover( props: PopoverProps ) { return ( <Popover {...props} className={( { isEntering, isExiting } ) => ` w-[280px] placement-bottom:mt-2 placement-top:mb-2 group rounded-lg drop-shadow-lg ring-1 ring-black/10 bg-white ${ isEntering ? 'animate-in fade-in placement-bottom:slide-in-from-top-1 placement-top:slide-in-from-bottom-1 ease-out duration-200' : '' } ${ isExiting ? 'animate-out fade-out placement-bottom:slide-out-to-top-1 placement-top:slide-out-to-bottom-1 ease-in duration-150' : '' } `} /> ); } function Notification( { avatar, name, time, text } ) { return ( <Link href="#" className="p-2 rounded-lg hover:bg-gray-100 grid grid-cols-[theme(width.5)_1fr_theme(width.4)] gap-x-2 text-[inherit] no-underline outline-hidden focus-visible:ring-2 ring-pink-800" > <img src={avatar} className="rounded-full w-5 h-5 row-span-3" /> <div className="text-gray-800 font-semibold leading-5"> {name} </div> <div className="text-gray-400"> <ChatIcon size="XS" /> </div> <div className="text-sm text-gray-500 col-span-2"> Commented {time} {' '} ago </div> <p className="text-sm overflow-hidden text-ellipsis line-clamp-2 mt-1 mb-0 col-span-2"> {text} </p> </Link> ); } ### Tailwind config# This example uses the following plugins: * tailwindcss-react-aria-components * tailwindcss-animate When using Tailwind v4, add them to your CSS: @import "tailwindcss"; @plugin "tailwindcss-react-aria-components"; @plugin "tailwindcss-animate"; @import "tailwindcss"; @plugin "tailwindcss-react-aria-components"; @plugin "tailwindcss-animate"; @import "tailwindcss"; @plugin "tailwindcss-react-aria-components"; @plugin "tailwindcss-animate"; Tailwind v3 When using Tailwind v3, add the plugins to your `tailwind.config.js` instead: module.exports = { // ... plugins: [ require('tailwindcss-react-aria-components'), require('tailwindcss-animate') ] }; module.exports = { // ... plugins: [ require('tailwindcss-react-aria-components'), require('tailwindcss-animate') ] }; module.exports = { // ... plugins: [ require( 'tailwindcss-react-aria-components' ), require( 'tailwindcss-animate' ) ] }; **Note**: When using Tailwind v3, install `tailwindcss-react-aria-components` version 1.x instead of 2.x. ## Components# * * * Popover A popover displays content in context with a trigger element. Dialog A dialog is an overlay shown above other content in an application. Button A button allows a user to perform an action. Link A link allows a user to navigate to another page. --- ## Page: https://react-spectrum.adobe.com/react-aria/examples/opacity-slider.html React AriaExamples # Opacity Slider An opacity Slider styled with Tailwind CSS. ## Example# * * * Opacity 30 import {Label, Slider, SliderOutput, SliderThumb, SliderTrack} from 'react-aria-components'; <div className="bg-linear-to-r from-purple-600 to-pink-600 p-12 rounded-lg flex justify-center"> <Slider defaultValue={30} className="w-[250px]"> <div className="flex text-white"> <Label className="flex-1">Opacity</Label> <SliderOutput /> </div> <SliderTrack className="relative w-full h-7"> {({ state }) => ( <> {/* track */} <div className="absolute h-2 top-[50%] translate-y-[-50%] w-full rounded-full bg-white/40" /> {/* fill */} <div className="absolute h-2 top-[50%] translate-y-[-50%] rounded-full bg-white" style={{ width: state.getThumbPercent(0) * 100 + '%' }} /> <SliderThumb className="h-7 w-7 top-[50%] rounded-full border border-solid border-purple-800/75 bg-white transition dragging:bg-purple-100 outline-hidden focus-visible:ring-2 ring-black" /> </> )} </SliderTrack> </Slider> </div> import { Label, Slider, SliderOutput, SliderThumb, SliderTrack } from 'react-aria-components'; <div className="bg-linear-to-r from-purple-600 to-pink-600 p-12 rounded-lg flex justify-center"> <Slider defaultValue={30} className="w-[250px]"> <div className="flex text-white"> <Label className="flex-1">Opacity</Label> <SliderOutput /> </div> <SliderTrack className="relative w-full h-7"> {({ state }) => ( <> {/* track */} <div className="absolute h-2 top-[50%] translate-y-[-50%] w-full rounded-full bg-white/40" /> {/* fill */} <div className="absolute h-2 top-[50%] translate-y-[-50%] rounded-full bg-white" style={{ width: state.getThumbPercent(0) * 100 + '%' }} /> <SliderThumb className="h-7 w-7 top-[50%] rounded-full border border-solid border-purple-800/75 bg-white transition dragging:bg-purple-100 outline-hidden focus-visible:ring-2 ring-black" /> </> )} </SliderTrack> </Slider> </div> import { Label, Slider, SliderOutput, SliderThumb, SliderTrack } from 'react-aria-components'; <div className="bg-linear-to-r from-purple-600 to-pink-600 p-12 rounded-lg flex justify-center"> <Slider defaultValue={30} className="w-[250px]" > <div className="flex text-white"> <Label className="flex-1"> Opacity </Label> <SliderOutput /> </div> <SliderTrack className="relative w-full h-7"> {({ state }) => ( <> {/* track */} <div className="absolute h-2 top-[50%] translate-y-[-50%] w-full rounded-full bg-white/40" /> {/* fill */} <div className="absolute h-2 top-[50%] translate-y-[-50%] rounded-full bg-white" style={{ width: state .getThumbPercent( 0 ) * 100 + '%' }} /> <SliderThumb className="h-7 w-7 top-[50%] rounded-full border border-solid border-purple-800/75 bg-white transition dragging:bg-purple-100 outline-hidden focus-visible:ring-2 ring-black" /> </> )} </SliderTrack> </Slider> </div> ### Tailwind config# This example uses the tailwindcss-react-aria-components plugin. When using Tailwind v4, add it to your CSS: @import "tailwindcss"; @plugin "tailwindcss-react-aria-components"; @import "tailwindcss"; @plugin "tailwindcss-react-aria-components"; @import "tailwindcss"; @plugin "tailwindcss-react-aria-components"; Tailwind v3 When using Tailwind v3, add the plugin to your `tailwind.config.js` instead: module.exports = { // ... plugins: [ require('tailwindcss-react-aria-components') ] }; module.exports = { // ... plugins: [ require('tailwindcss-react-aria-components') ] }; module.exports = { // ... plugins: [ require( 'tailwindcss-react-aria-components' ) ] }; **Note**: When using Tailwind v3, install `tailwindcss-react-aria-components` version 1.x instead of 2.x. ## Components# * * * Slider A slider allows a user to select one or more values within a range. --- ## Page: https://react-spectrum.adobe.com/react-aria/examples/ripple-button.html React AriaExamples # Ripple Button A Button with an animated ripple effect styled with Tailwind CSS. ## Example# * * * Book flight import {Button} from 'react-aria-components'; import {useEffect, useRef, useState} from 'react'; import Airplane from '@spectrum-icons/workflow/Airplane'; function RippleButton(props) { const [coords, setCoords] = useState(null); let timeout = useRef<ReturnType<typeof setTimeout> | null>(null); let onPress = (e) => { setCoords({ x: e.x, y: e.y }); if (e.x !== -1 && e.y !== -1) { clearTimeout(timeout.current); timeout.current = setTimeout(() => setCoords(null), 600); } }; useEffect(() => { return () => { clearTimeout(timeout.current); }; }, []); return ( <div className="bg-linear-to-r from-teal-300 to-cyan-500 p-12 rounded-lg flex justify-center"> <Button onPress={onPress} className={` relative overflow-hidden inline-flex items-center justify-center rounded-md bg-black/50 bg-clip-padding border border-white/20 px-6 py-4 text-white text-lg hover:bg-black/60 pressed:bg-black/70 transition-colors cursor-default outline-hidden focus-visible:ring-2 focus-visible:ring-white/75`} > {coords && ( <div key={`${coords.x},${coords.y}`} className="absolute h-8 w-8 rounded-full opacity-100 bg-white/60" style={{ animation: 'ripple 600ms linear', left: coords.x - 15, top: coords.y - 15 }} /> )} <span className="flex items-center gap-4">{props.children}</span> </Button> </div> ); } <RippleButton> <Airplane size="S" /> Book flight </RippleButton> import {Button} from 'react-aria-components'; import {useEffect, useRef, useState} from 'react'; import Airplane from '@spectrum-icons/workflow/Airplane'; function RippleButton(props) { const [coords, setCoords] = useState(null); let timeout = useRef< ReturnType<typeof setTimeout> | null >(null); let onPress = (e) => { setCoords({ x: e.x, y: e.y }); if (e.x !== -1 && e.y !== -1) { clearTimeout(timeout.current); timeout.current = setTimeout( () => setCoords(null), 600 ); } }; useEffect(() => { return () => { clearTimeout(timeout.current); }; }, []); return ( <div className="bg-linear-to-r from-teal-300 to-cyan-500 p-12 rounded-lg flex justify-center"> <Button onPress={onPress} className={` relative overflow-hidden inline-flex items-center justify-center rounded-md bg-black/50 bg-clip-padding border border-white/20 px-6 py-4 text-white text-lg hover:bg-black/60 pressed:bg-black/70 transition-colors cursor-default outline-hidden focus-visible:ring-2 focus-visible:ring-white/75`} > {coords && ( <div key={`${coords.x},${coords.y}`} className="absolute h-8 w-8 rounded-full opacity-100 bg-white/60" style={{ animation: 'ripple 600ms linear', left: coords.x - 15, top: coords.y - 15 }} /> )} <span className="flex items-center gap-4"> {props.children} </span> </Button> </div> ); } <RippleButton> <Airplane size="S" /> Book flight </RippleButton> import {Button} from 'react-aria-components'; import { useEffect, useRef, useState } from 'react'; import Airplane from '@spectrum-icons/workflow/Airplane'; function RippleButton( props ) { const [ coords, setCoords ] = useState(null); let timeout = useRef< ReturnType< typeof setTimeout > | null >(null); let onPress = (e) => { setCoords({ x: e.x, y: e.y }); if ( e.x !== -1 && e.y !== -1 ) { clearTimeout( timeout.current ); timeout.current = setTimeout( () => setCoords( null ), 600 ); } }; useEffect(() => { return () => { clearTimeout( timeout.current ); }; }, []); return ( <div className="bg-linear-to-r from-teal-300 to-cyan-500 p-12 rounded-lg flex justify-center"> <Button onPress={onPress} className={` relative overflow-hidden inline-flex items-center justify-center rounded-md bg-black/50 bg-clip-padding border border-white/20 px-6 py-4 text-white text-lg hover:bg-black/60 pressed:bg-black/70 transition-colors cursor-default outline-hidden focus-visible:ring-2 focus-visible:ring-white/75`} > {coords && ( <div key={`${coords.x},${coords.y}`} className="absolute h-8 w-8 rounded-full opacity-100 bg-white/60" style={{ animation: 'ripple 600ms linear', left: coords .x - 15, top: coords .y - 15 }} /> )} <span className="flex items-center gap-4"> {props .children} </span> </Button> </div> ); } <RippleButton> <Airplane size="S" /> {' '} Book flight </RippleButton> @keyframes ripple { from { transform: scale(0); opacity: 1; } to { transform: scale(6); opacity: 0; } } @keyframes ripple { from { transform: scale(0); opacity: 1; } to { transform: scale(6); opacity: 0; } } @keyframes ripple { from { transform: scale(0); opacity: 1; } to { transform: scale(6); opacity: 0; } } ### Tailwind config# This example uses the tailwindcss-react-aria-components plugin. When using Tailwind v4, add it to your CSS: @import "tailwindcss"; @plugin "tailwindcss-react-aria-components"; @import "tailwindcss"; @plugin "tailwindcss-react-aria-components"; @import "tailwindcss"; @plugin "tailwindcss-react-aria-components"; Tailwind v3 When using Tailwind v3, add the plugin to your `tailwind.config.js` instead: module.exports = { // ... plugins: [ require('tailwindcss-react-aria-components') ] }; module.exports = { // ... plugins: [ require('tailwindcss-react-aria-components') ] }; module.exports = { // ... plugins: [ require( 'tailwindcss-react-aria-components' ) ] }; **Note**: When using Tailwind v3, install `tailwindcss-react-aria-components` version 1.x instead of 2.x. ## Components# * * * Button A button allows a user to perform an action. --- ## Page: https://react-spectrum.adobe.com/react-aria/examples/searchable-select.html React AriaExamples # Searchable Select A Select component with Autocomplete filtering. ## Example# * * * LanguageSelect an item ArabicBengaliBosnianCzechDanishGermanGreekEnglishSpanishPersianFinnishFrenchGujaratiHindiCroatianHungarianIcelandicItalianJapaneseJavaneseKannadaKoreanMalayalamMarathiNorwegianPunjabiPolishPortugueseRomanianRussianSlovakSlovenianAlbanianSerbianSwedishSwahiliTamilTeluguThaiFilipinoTurkishUkrainianUrduVietnameseChinese import type {ListBoxItemProps} from 'react-aria-components'; import {Autocomplete, Button, Input, Label, ListBox, ListBoxItem, Popover, SearchField, Select, SelectValue, useFilter} from 'react-aria-components'; import {CheckIcon, CheckIcon, ChevronsUpDownIcon, SearchIcon, XIcon} from 'lucide-react'; function SelectExample() { let { contains } = useFilter({ sensitivity: 'base' }); return ( <div className="bg-linear-to-br from-cyan-200 to-blue-400 p-8 sm:h-[350px] rounded-lg flex justify-center"> <Select className="flex flex-col gap-1 w-[200px]"> <Label className="text-black cursor-default">Language</Label> <Button className="flex items-center cursor-default rounded-lg border-0 bg-white/90 pressed:bg-white transition py-2 pl-5 pr-2 text-base text-left leading-normal ring-1 ring-black/5 shadow-md text-gray-700 focus:outline-hidden focus-visible:outline-2 outline-black outline-offset-3 focus-visible:ring-black/25"> <SelectValue className="flex-1 truncate" /> <ChevronsUpDownIcon className="w-4 h-4" /> </Button> <Popover className="!max-h-80 w-(--trigger-width) flex flex-col rounded-md bg-white text-base shadow-lg ring-1 ring-black/5 entering:animate-in entering:fade-in exiting:animate-out exiting:fade-out"> <Autocomplete filter={contains}> <SearchField aria-label="Search" autoFocus className="group flex items-center bg-white forced-colors:bg-[Field] border-2 border-gray-300 has-focus:border-sky-600 rounded-full m-1" > <SearchIcon aria-hidden className="w-4 h-4 ml-2 text-gray-600 forced-colors:text-[ButtonText]" /> <Input placeholder="Search languages" className="px-2 py-1 flex-1 min-w-0 border-none outline outline-0 bg-white text-base text-gray-800 placeholder-gray-500 font-[inherit] [&::-webkit-search-cancel-button]:hidden" /> <Button className="text-sm text-center transition rounded-full border-0 p-1 flex items-center justify-center text-gray-600 bg-transparent hover:bg-black/[5%] pressed:bg-black/10 mr-1 w-6 group-empty:invisible"> <XIcon aria-hidden className="w-4 h-4" /> </Button> </SearchField> <ListBox items={languages} className="outline-hidden p-1 overflow-auto flex-1 scroll-pb-1" > {(item) => <SelectItem>{item.name}</SelectItem>} </ListBox> </Autocomplete> </Popover> </Select> </div> ); } function SelectItem(props: ListBoxItemProps & { children: string }) { return ( <ListBoxItem {...props} textValue={props.children} className="group flex items-center gap-2 cursor-default select-none py-2 px-4 outline-hidden rounded-sm text-gray-900 focus:bg-sky-600 focus:text-white" > {({ isSelected }) => ( <> <span className="flex-1 flex items-center gap-2 truncate font-normal group-selected:font-medium"> {props.children} </span> <span className="w-5 flex items-center text-sky-600 group-focus:text-white"> {isSelected && <CheckIcon size="S" />} </span> </> )} </ListBoxItem> ); } import type {ListBoxItemProps} from 'react-aria-components'; import { Autocomplete, Button, Input, Label, ListBox, ListBoxItem, Popover, SearchField, Select, SelectValue, useFilter } from 'react-aria-components'; import { CheckIcon, CheckIcon, ChevronsUpDownIcon, SearchIcon, XIcon } from 'lucide-react'; function SelectExample() { let { contains } = useFilter({ sensitivity: 'base' }); return ( <div className="bg-linear-to-br from-cyan-200 to-blue-400 p-8 sm:h-[350px] rounded-lg flex justify-center"> <Select className="flex flex-col gap-1 w-[200px]"> <Label className="text-black cursor-default"> Language </Label> <Button className="flex items-center cursor-default rounded-lg border-0 bg-white/90 pressed:bg-white transition py-2 pl-5 pr-2 text-base text-left leading-normal ring-1 ring-black/5 shadow-md text-gray-700 focus:outline-hidden focus-visible:outline-2 outline-black outline-offset-3 focus-visible:ring-black/25"> <SelectValue className="flex-1 truncate" /> <ChevronsUpDownIcon className="w-4 h-4" /> </Button> <Popover className="!max-h-80 w-(--trigger-width) flex flex-col rounded-md bg-white text-base shadow-lg ring-1 ring-black/5 entering:animate-in entering:fade-in exiting:animate-out exiting:fade-out"> <Autocomplete filter={contains}> <SearchField aria-label="Search" autoFocus className="group flex items-center bg-white forced-colors:bg-[Field] border-2 border-gray-300 has-focus:border-sky-600 rounded-full m-1" > <SearchIcon aria-hidden className="w-4 h-4 ml-2 text-gray-600 forced-colors:text-[ButtonText]" /> <Input placeholder="Search languages" className="px-2 py-1 flex-1 min-w-0 border-none outline outline-0 bg-white text-base text-gray-800 placeholder-gray-500 font-[inherit] [&::-webkit-search-cancel-button]:hidden" /> <Button className="text-sm text-center transition rounded-full border-0 p-1 flex items-center justify-center text-gray-600 bg-transparent hover:bg-black/[5%] pressed:bg-black/10 mr-1 w-6 group-empty:invisible"> <XIcon aria-hidden className="w-4 h-4" /> </Button> </SearchField> <ListBox items={languages} className="outline-hidden p-1 overflow-auto flex-1 scroll-pb-1" > {(item) => ( <SelectItem>{item.name}</SelectItem> )} </ListBox> </Autocomplete> </Popover> </Select> </div> ); } function SelectItem( props: ListBoxItemProps & { children: string } ) { return ( <ListBoxItem {...props} textValue={props.children} className="group flex items-center gap-2 cursor-default select-none py-2 px-4 outline-hidden rounded-sm text-gray-900 focus:bg-sky-600 focus:text-white" > {({ isSelected }) => ( <> <span className="flex-1 flex items-center gap-2 truncate font-normal group-selected:font-medium"> {props.children} </span> <span className="w-5 flex items-center text-sky-600 group-focus:text-white"> {isSelected && <CheckIcon size="S" />} </span> </> )} </ListBoxItem> ); } import type {ListBoxItemProps} from 'react-aria-components'; import { Autocomplete, Button, Input, Label, ListBox, ListBoxItem, Popover, SearchField, Select, SelectValue, useFilter } from 'react-aria-components'; import { CheckIcon, CheckIcon, ChevronsUpDownIcon, SearchIcon, XIcon } from 'lucide-react'; function SelectExample() { let { contains } = useFilter({ sensitivity: 'base' }); return ( <div className="bg-linear-to-br from-cyan-200 to-blue-400 p-8 sm:h-[350px] rounded-lg flex justify-center"> <Select className="flex flex-col gap-1 w-[200px]"> <Label className="text-black cursor-default"> Language </Label> <Button className="flex items-center cursor-default rounded-lg border-0 bg-white/90 pressed:bg-white transition py-2 pl-5 pr-2 text-base text-left leading-normal ring-1 ring-black/5 shadow-md text-gray-700 focus:outline-hidden focus-visible:outline-2 outline-black outline-offset-3 focus-visible:ring-black/25"> <SelectValue className="flex-1 truncate" /> <ChevronsUpDownIcon className="w-4 h-4" /> </Button> <Popover className="!max-h-80 w-(--trigger-width) flex flex-col rounded-md bg-white text-base shadow-lg ring-1 ring-black/5 entering:animate-in entering:fade-in exiting:animate-out exiting:fade-out"> <Autocomplete filter={contains} > <SearchField aria-label="Search" autoFocus className="group flex items-center bg-white forced-colors:bg-[Field] border-2 border-gray-300 has-focus:border-sky-600 rounded-full m-1" > <SearchIcon aria-hidden className="w-4 h-4 ml-2 text-gray-600 forced-colors:text-[ButtonText]" /> <Input placeholder="Search languages" className="px-2 py-1 flex-1 min-w-0 border-none outline outline-0 bg-white text-base text-gray-800 placeholder-gray-500 font-[inherit] [&::-webkit-search-cancel-button]:hidden" /> <Button className="text-sm text-center transition rounded-full border-0 p-1 flex items-center justify-center text-gray-600 bg-transparent hover:bg-black/[5%] pressed:bg-black/10 mr-1 w-6 group-empty:invisible"> <XIcon aria-hidden className="w-4 h-4" /> </Button> </SearchField> <ListBox items={languages} className="outline-hidden p-1 overflow-auto flex-1 scroll-pb-1" > {(item) => ( <SelectItem> {item .name} </SelectItem> )} </ListBox> </Autocomplete> </Popover> </Select> </div> ); } function SelectItem( props: & ListBoxItemProps & { children: string; } ) { return ( <ListBoxItem {...props} textValue={props .children} className="group flex items-center gap-2 cursor-default select-none py-2 px-4 outline-hidden rounded-sm text-gray-900 focus:bg-sky-600 focus:text-white" > {( { isSelected } ) => ( <> <span className="flex-1 flex items-center gap-2 truncate font-normal group-selected:font-medium"> {props .children} </span> <span className="w-5 flex items-center text-sky-600 group-focus:text-white"> {isSelected && ( <CheckIcon size="S" /> )} </span> </> )} </ListBoxItem> ); } ### Tailwind config# This example uses the following plugins: * tailwindcss-react-aria-components * tailwindcss-animate When using Tailwind v4, add them to your CSS: @import "tailwindcss"; @plugin "tailwindcss-react-aria-components"; @plugin "tailwindcss-animate"; @import "tailwindcss"; @plugin "tailwindcss-react-aria-components"; @plugin "tailwindcss-animate"; @import "tailwindcss"; @plugin "tailwindcss-react-aria-components"; @plugin "tailwindcss-animate"; Tailwind v3 When using Tailwind v3, add the plugins to your `tailwind.config.js` instead: module.exports = { // ... plugins: [ require('tailwindcss-react-aria-components'), require('tailwindcss-animate') ] }; module.exports = { // ... plugins: [ require('tailwindcss-react-aria-components'), require('tailwindcss-animate') ] }; module.exports = { // ... plugins: [ require( 'tailwindcss-react-aria-components' ), require( 'tailwindcss-animate' ) ] }; **Note**: When using Tailwind v3, install `tailwindcss-react-aria-components` version 1.x instead of 2.x. ## Components# * * * Select A select displays a collapsible list of options, and allows a user to select one of them. ListBox A listbox allows a user to select one or more options from a list. Popover A popover displays content in context with a trigger element. Button A button allows a user to perform an action. SearchField A search field allows a user to enter and clear a search query. --- ## Page: https://react-spectrum.adobe.com/react-aria/examples/shipping-radio.html React AriaExamples # Shipping Radio Group A shipping options RadioGroup styled with Tailwind CSS. ## Example# * * * Shipping Standard 4-10 business days $4.99 Express 2-5 business days $15.99 Lightning 1 business day $24.99 import {Label, Radio, RadioGroup} from 'react-aria-components'; import CheckCircleIcon from '@spectrum-icons/workflow/CheckmarkCircle'; function RadioGroupExample() { return ( <div className="bg-linear-to-r from-blue-300 to-indigo-300 p-2 sm:p-8 rounded-lg flex justify-center"> <RadioGroup className="flex flex-col gap-2 w-full max-w-[300px]" defaultValue="Standard" > <Label className="text-xl text-slate-900 font-semibold font-serif"> Shipping </Label> <ShippingOption name="Standard" time="4-10 business days" price="$4.99" /> <ShippingOption name="Express" time="2-5 business days" price="$15.99" /> <ShippingOption name="Lightning" time="1 business day" price="$24.99" /> </RadioGroup> </div> ); } function ShippingOption({ name, time, price }) { return ( <Radio value={name} className={({ isFocusVisible, isSelected, isPressed }) => ` group relative flex cursor-default rounded-lg px-4 py-3 shadow-lg outline-hidden bg-clip-padding border border-solid ${ isFocusVisible ? 'ring-2 ring-blue-600 ring-offset-1 ring-offset-white/80' : '' } ${ isSelected ? 'bg-blue-600 border-white/30 text-white' : 'border-transparent' } ${isPressed && !isSelected ? 'bg-blue-50' : ''} ${!isSelected && !isPressed ? 'bg-white' : ''} `} > <div className="flex w-full items-center justify-between gap-3"> <div className="flex items-center shrink-0 text-blue-100 group-selected:text-white"> <CheckCircleIcon size="M" /> </div> <div className="flex flex-1 flex-col"> <div className="text-lg font-serif font-semibold text-gray-900 group-selected:text-white"> {name} </div> <div className="inline text-gray-500 group-selected:text-sky-100"> {time} </div> </div> <div className="font-medium font-mono text-gray-900 group-selected:text-white"> {price} </div> </div> </Radio> ); } import { Label, Radio, RadioGroup } from 'react-aria-components'; import CheckCircleIcon from '@spectrum-icons/workflow/CheckmarkCircle'; function RadioGroupExample() { return ( <div className="bg-linear-to-r from-blue-300 to-indigo-300 p-2 sm:p-8 rounded-lg flex justify-center"> <RadioGroup className="flex flex-col gap-2 w-full max-w-[300px]" defaultValue="Standard" > <Label className="text-xl text-slate-900 font-semibold font-serif"> Shipping </Label> <ShippingOption name="Standard" time="4-10 business days" price="$4.99" /> <ShippingOption name="Express" time="2-5 business days" price="$15.99" /> <ShippingOption name="Lightning" time="1 business day" price="$24.99" /> </RadioGroup> </div> ); } function ShippingOption({ name, time, price }) { return ( <Radio value={name} className={( { isFocusVisible, isSelected, isPressed } ) => ` group relative flex cursor-default rounded-lg px-4 py-3 shadow-lg outline-hidden bg-clip-padding border border-solid ${ isFocusVisible ? 'ring-2 ring-blue-600 ring-offset-1 ring-offset-white/80' : '' } ${ isSelected ? 'bg-blue-600 border-white/30 text-white' : 'border-transparent' } ${isPressed && !isSelected ? 'bg-blue-50' : ''} ${!isSelected && !isPressed ? 'bg-white' : ''} `} > <div className="flex w-full items-center justify-between gap-3"> <div className="flex items-center shrink-0 text-blue-100 group-selected:text-white"> <CheckCircleIcon size="M" /> </div> <div className="flex flex-1 flex-col"> <div className="text-lg font-serif font-semibold text-gray-900 group-selected:text-white"> {name} </div> <div className="inline text-gray-500 group-selected:text-sky-100"> {time} </div> </div> <div className="font-medium font-mono text-gray-900 group-selected:text-white"> {price} </div> </div> </Radio> ); } import { Label, Radio, RadioGroup } from 'react-aria-components'; import CheckCircleIcon from '@spectrum-icons/workflow/CheckmarkCircle'; function RadioGroupExample() { return ( <div className="bg-linear-to-r from-blue-300 to-indigo-300 p-2 sm:p-8 rounded-lg flex justify-center"> <RadioGroup className="flex flex-col gap-2 w-full max-w-[300px]" defaultValue="Standard" > <Label className="text-xl text-slate-900 font-semibold font-serif"> Shipping </Label> <ShippingOption name="Standard" time="4-10 business days" price="$4.99" /> <ShippingOption name="Express" time="2-5 business days" price="$15.99" /> <ShippingOption name="Lightning" time="1 business day" price="$24.99" /> </RadioGroup> </div> ); } function ShippingOption( { name, time, price } ) { return ( <Radio value={name} className={( { isFocusVisible, isSelected, isPressed } ) => ` group relative flex cursor-default rounded-lg px-4 py-3 shadow-lg outline-hidden bg-clip-padding border border-solid ${ isFocusVisible ? 'ring-2 ring-blue-600 ring-offset-1 ring-offset-white/80' : '' } ${ isSelected ? 'bg-blue-600 border-white/30 text-white' : 'border-transparent' } ${ isPressed && !isSelected ? 'bg-blue-50' : '' } ${ !isSelected && !isPressed ? 'bg-white' : '' } `} > <div className="flex w-full items-center justify-between gap-3"> <div className="flex items-center shrink-0 text-blue-100 group-selected:text-white"> <CheckCircleIcon size="M" /> </div> <div className="flex flex-1 flex-col"> <div className="text-lg font-serif font-semibold text-gray-900 group-selected:text-white"> {name} </div> <div className="inline text-gray-500 group-selected:text-sky-100"> {time} </div> </div> <div className="font-medium font-mono text-gray-900 group-selected:text-white"> {price} </div> </div> </Radio> ); } ### Tailwind config# This example uses the tailwindcss-react-aria-components plugin. When using Tailwind v4, add it to your CSS: @import "tailwindcss"; @plugin "tailwindcss-react-aria-components"; @import "tailwindcss"; @plugin "tailwindcss-react-aria-components"; @import "tailwindcss"; @plugin "tailwindcss-react-aria-components"; Tailwind v3 When using Tailwind v3, add the plugin to your `tailwind.config.js` instead: module.exports = { // ... plugins: [ require('tailwindcss-react-aria-components') ] }; module.exports = { // ... plugins: [ require('tailwindcss-react-aria-components') ] }; module.exports = { // ... plugins: [ require( 'tailwindcss-react-aria-components' ) ] }; **Note**: When using Tailwind v3, install `tailwindcss-react-aria-components` version 1.x instead of 2.x. ## Components# * * * RadioGroup A radio group allows a user to select a single item from a list of options. --- ## Page: https://react-spectrum.adobe.com/react-aria/examples/status-select.html React AriaExamples # Status Select An issue status Select styled with Tailwind CSS. ## Example# * * * StatusSelect an item BacklogIn ProgressIn ReviewDoneWon't Do import {Button, Label, ListBox, ListBoxItem, Popover, Select, SelectValue} from 'react-aria-components'; import type {ListBoxItemProps} from 'react-aria-components'; import ChevronUpDownIcon from '@spectrum-icons/workflow/ChevronUpDown'; import CheckIcon from '@spectrum-icons/workflow/Checkmark'; function SelectExample() { return ( <div className="bg-linear-to-tl from-amber-500 to-rose-700 p-8 sm:h-[250px] rounded-lg flex justify-center"> <Select className="flex flex-col gap-1 w-[200px]"> <Label className="text-white cursor-default">Status</Label> <Button className="flex items-center cursor-default rounded-lg border-0 bg-white/90 pressed:bg-white transition py-2 pl-5 pr-2 text-base text-left leading-normal shadow-md text-gray-700 focus:outline-hidden focus-visible:ring-2 ring-white ring-offset-2 ring-offset-rose-700"> <SelectValue className="flex-1 truncate placeholder-shown:italic" /> <ChevronUpDownIcon size="XS" /> </Button> <Popover className="max-h-60 w-(--trigger-width) overflow-auto rounded-md bg-white text-base shadow-lg ring-1 ring-black/5 entering:animate-in entering:fade-in exiting:animate-out exiting:fade-out"> <ListBox className="outline-hidden p-1"> <StatusItem textValue="Backlog"> <Status className="bg-gray-500" /> Backlog </StatusItem> <StatusItem textValue="In Progress"> <Status className="bg-blue-500" /> In Progress </StatusItem> <StatusItem textValue="In Review"> <Status className="bg-yellow-500" /> In Review </StatusItem> <StatusItem textValue="Done"> <Status className="bg-green-500" /> Done </StatusItem> <StatusItem textValue="Won't Do"> <Status className="bg-red-500" /> Won't Do </StatusItem> </ListBox> </Popover> </Select> </div> ); } function StatusItem(props: ListBoxItemProps & { children: React.ReactNode }) { return ( <ListBoxItem {...props} className="group flex items-center gap-2 cursor-default select-none py-2 px-4 outline-hidden rounded-sm text-gray-900 focus:bg-rose-600 focus:text-white" > {({ isSelected }) => ( <> <span className="flex-1 flex items-center gap-2 truncate font-normal group-selected:font-medium"> {props.children} </span> <span className="w-5 flex items-center text-rose-600 group-focus:text-white"> {isSelected && <CheckIcon size="S" />} </span> </> )} </ListBoxItem> ); } function Status({ className }: { className: string }) { return ( <span className={`w-3 h-3 rounded-full border border-solid border-white ${className}`} /> ); } import { Button, Label, ListBox, ListBoxItem, Popover, Select, SelectValue } from 'react-aria-components'; import type {ListBoxItemProps} from 'react-aria-components'; import ChevronUpDownIcon from '@spectrum-icons/workflow/ChevronUpDown'; import CheckIcon from '@spectrum-icons/workflow/Checkmark'; function SelectExample() { return ( <div className="bg-linear-to-tl from-amber-500 to-rose-700 p-8 sm:h-[250px] rounded-lg flex justify-center"> <Select className="flex flex-col gap-1 w-[200px]"> <Label className="text-white cursor-default"> Status </Label> <Button className="flex items-center cursor-default rounded-lg border-0 bg-white/90 pressed:bg-white transition py-2 pl-5 pr-2 text-base text-left leading-normal shadow-md text-gray-700 focus:outline-hidden focus-visible:ring-2 ring-white ring-offset-2 ring-offset-rose-700"> <SelectValue className="flex-1 truncate placeholder-shown:italic" /> <ChevronUpDownIcon size="XS" /> </Button> <Popover className="max-h-60 w-(--trigger-width) overflow-auto rounded-md bg-white text-base shadow-lg ring-1 ring-black/5 entering:animate-in entering:fade-in exiting:animate-out exiting:fade-out"> <ListBox className="outline-hidden p-1"> <StatusItem textValue="Backlog"> <Status className="bg-gray-500" /> Backlog </StatusItem> <StatusItem textValue="In Progress"> <Status className="bg-blue-500" /> In Progress </StatusItem> <StatusItem textValue="In Review"> <Status className="bg-yellow-500" /> In Review </StatusItem> <StatusItem textValue="Done"> <Status className="bg-green-500" /> Done </StatusItem> <StatusItem textValue="Won't Do"> <Status className="bg-red-500" /> Won't Do </StatusItem> </ListBox> </Popover> </Select> </div> ); } function StatusItem( props: ListBoxItemProps & { children: React.ReactNode } ) { return ( <ListBoxItem {...props} className="group flex items-center gap-2 cursor-default select-none py-2 px-4 outline-hidden rounded-sm text-gray-900 focus:bg-rose-600 focus:text-white" > {({ isSelected }) => ( <> <span className="flex-1 flex items-center gap-2 truncate font-normal group-selected:font-medium"> {props.children} </span> <span className="w-5 flex items-center text-rose-600 group-focus:text-white"> {isSelected && <CheckIcon size="S" />} </span> </> )} </ListBoxItem> ); } function Status({ className }: { className: string }) { return ( <span className={`w-3 h-3 rounded-full border border-solid border-white ${className}`} /> ); } import { Button, Label, ListBox, ListBoxItem, Popover, Select, SelectValue } from 'react-aria-components'; import type {ListBoxItemProps} from 'react-aria-components'; import ChevronUpDownIcon from '@spectrum-icons/workflow/ChevronUpDown'; import CheckIcon from '@spectrum-icons/workflow/Checkmark'; function SelectExample() { return ( <div className="bg-linear-to-tl from-amber-500 to-rose-700 p-8 sm:h-[250px] rounded-lg flex justify-center"> <Select className="flex flex-col gap-1 w-[200px]"> <Label className="text-white cursor-default"> Status </Label> <Button className="flex items-center cursor-default rounded-lg border-0 bg-white/90 pressed:bg-white transition py-2 pl-5 pr-2 text-base text-left leading-normal shadow-md text-gray-700 focus:outline-hidden focus-visible:ring-2 ring-white ring-offset-2 ring-offset-rose-700"> <SelectValue className="flex-1 truncate placeholder-shown:italic" /> <ChevronUpDownIcon size="XS" /> </Button> <Popover className="max-h-60 w-(--trigger-width) overflow-auto rounded-md bg-white text-base shadow-lg ring-1 ring-black/5 entering:animate-in entering:fade-in exiting:animate-out exiting:fade-out"> <ListBox className="outline-hidden p-1"> <StatusItem textValue="Backlog"> <Status className="bg-gray-500" /> Backlog </StatusItem> <StatusItem textValue="In Progress"> <Status className="bg-blue-500" /> In Progress </StatusItem> <StatusItem textValue="In Review"> <Status className="bg-yellow-500" /> In Review </StatusItem> <StatusItem textValue="Done"> <Status className="bg-green-500" /> Done </StatusItem> <StatusItem textValue="Won't Do"> <Status className="bg-red-500" /> Won't Do </StatusItem> </ListBox> </Popover> </Select> </div> ); } function StatusItem( props: & ListBoxItemProps & { children: React.ReactNode; } ) { return ( <ListBoxItem {...props} className="group flex items-center gap-2 cursor-default select-none py-2 px-4 outline-hidden rounded-sm text-gray-900 focus:bg-rose-600 focus:text-white" > {( { isSelected } ) => ( <> <span className="flex-1 flex items-center gap-2 truncate font-normal group-selected:font-medium"> {props .children} </span> <span className="w-5 flex items-center text-rose-600 group-focus:text-white"> {isSelected && ( <CheckIcon size="S" /> )} </span> </> )} </ListBoxItem> ); } function Status( { className }: { className: string; } ) { return ( <span className={`w-3 h-3 rounded-full border border-solid border-white ${className}`} /> ); } ### Tailwind config# This example uses the following plugins: * tailwindcss-react-aria-components * tailwindcss-animate When using Tailwind v4, add them to your CSS: @import "tailwindcss"; @plugin "tailwindcss-react-aria-components"; @plugin "tailwindcss-animate"; @import "tailwindcss"; @plugin "tailwindcss-react-aria-components"; @plugin "tailwindcss-animate"; @import "tailwindcss"; @plugin "tailwindcss-react-aria-components"; @plugin "tailwindcss-animate"; Tailwind v3 When using Tailwind v3, add the plugins to your `tailwind.config.js` instead: module.exports = { // ... plugins: [ require('tailwindcss-react-aria-components'), require('tailwindcss-animate') ] }; module.exports = { // ... plugins: [ require('tailwindcss-react-aria-components'), require('tailwindcss-animate') ] }; module.exports = { // ... plugins: [ require( 'tailwindcss-react-aria-components' ), require( 'tailwindcss-animate' ) ] }; **Note**: When using Tailwind v3, install `tailwindcss-react-aria-components` version 1.x instead of 2.x. ## Components# * * * Select A select displays a collapsible list of options, and allows a user to select one of them. ListBox A listbox allows a user to select one or more options from a list. Popover A popover displays content in context with a trigger element. Button A button allows a user to perform an action. --- ## Page: https://react-spectrum.adobe.com/react-aria/examples/stock-table.html React AriaExamples # Stock Table A stock Table featuring sticky headers, sorting, multiple selection, and column resizing, styled with Tailwind CSS. ## Example# * * * | Symbol | Name | Market Cap | Sector | Industry | | --- | --- | --- | --- | --- | | $ATNX | Athenex, Inc. | $767.4M | n/a | n/a | | $AUY | Yamana Gold Inc. | $2.32B | Basic Industries | Precious Metals | | $BIOC | Biocept, Inc. | $32.98M | Health Care | Medical Specialities | | $DCM | NTT DOCOMO, Inc | $96.67B | Technology | Radio And Television Broadcasting And Communications Equipment | | $DNI | Dividend and Income Fund | $130.45M | n/a | n/a | | $DPG | Duff & Phelps Global Utility Income Fund Inc. | $626.98M | n/a | n/a | | $EFSC | Enterprise Financial Services Corporation | $965.1M | Finance | Major Banks | | $EPE | EP Energy Corporation | $1.02B | Energy | Oil & Gas Production | | $EQC | Equity Commonwealth | $3.93B | Consumer Services | Real Estate Investment Trusts | | $ETH | Ethan Allen Interiors Inc. | $822.58M | Consumer Durables | Home Furnishings | | $FENX | Fenix Parts, Inc. | $29.61M | Consumer Services | Motor Vehicles | | $FTRPR | Frontier Communications Corporation | n/a | Public Utilities | Telecommunications Equipment | | $GNW | Genworth Financial Inc | $1.82B | Finance | Life Insurance | | $GPIAU | GP Investments Acquisition Corp. | n/a | Consumer Durables | Home Furnishings | | $HIIQ | Health Insurance Innovations, Inc. | $392.38M | Finance | Specialty Insurers | | $JNP | Juniper Pharmaceuticals, Inc. | $55.3M | Health Care | Major Pharmaceuticals | | $KAP | KCAP Financial, Inc. | n/a | n/a | n/a | | $KNL | Knoll, Inc. | $1.04B | Consumer Durables | Office Equipment / Supplies / Services | | $KOP | Koppers Holdings Inc. | $716.78M | Basic Industries | Forest Products | | $KRA | Kraton Corporation | $979.78M | Basic Industries | Major Chemicals | | $MANH | Manhattan Associates, Inc. | $3.27B | Technology | Computer Software: Prepackaged Software | | $MARK | Remark Holdings, Inc. | $57.31M | Consumer Services | Telecommunications Equipment | | $MERC | Mercer International Inc. | $769.94M | Basic Industries | Paper | | $MOFG | MidWestOne Financial Group, Inc. | $437.4M | Finance | Major Banks | | $NFJ | AllianzGI NFJ Dividend, Interest & Premium Strategy Fund | $1.24B | Finance | Finance: Consumer Services | | $NMK^B | Niagara Mohawk Holdings, Inc. | n/a | Public Utilities | Power Generation | | $NNN | National Retail Properties | $5.87B | Consumer Services | Real Estate Investment Trusts | | $NVTR | Nuvectra Corporation | $132.49M | Health Care | Medical/Dental Instruments | | $PAACR | Pacific Special Acquisition Corp. | n/a | Finance | Business Services | | $PAH | Platform Specialty Products Corporation | $3.52B | Basic Industries | Major Chemicals | | $PBI | Pitney Bowes Inc. | $2.84B | Miscellaneous | Office Equipment / Supplies / Services | | $PNF | PIMCO New York Municipal Income Fund | $99.42M | n/a | n/a | | $PSA^Y | Public Storage | n/a | n/a | n/a | | $RFEU | First Trust RiverFront Dynamic Europe ETF | $52.66M | n/a | n/a | | $RGLS | Regulus Therapeutics Inc. | $50.52M | Health Care | Major Pharmaceuticals | | $SAB | Saratoga Investment Corp | n/a | n/a | n/a | | $SODA | SodaStream International Ltd. | $1.13B | Consumer Durables | Consumer Electronics/Appliances | | $SSB | South State Corporation | $2.55B | Finance | Major Banks | | $TBPH | Theravance Biopharma, Inc. | $1.97B | Health Care | Major Pharmaceuticals | | $TEO | Telecom Argentina Stet - France Telecom S.A. | $4.83B | Public Utilities | Telecommunications Equipment | | $THQ | Tekla Healthcare Opportunies Fund | $772.41M | n/a | n/a | | $TNP^C | Tsakos Energy Navigation Ltd | n/a | n/a | n/a | | $TWNK | Hostess Brands, Inc. | $2.09B | Consumer Non-Durables | Packaged Foods | | $ULBI | Ultralife Corporation | $102.3M | Miscellaneous | Industrial Machinery/Components | | $USDP | USD Partners LP | $300.48M | Transportation | Railroads | | $VNQI | Vanguard Global ex-U.S. Real Estate ETF | $4.39B | n/a | n/a | | $VRTS | Virtus Investment Partners, Inc. | $785.49M | Finance | Investment Managers | | $WF | Woori Bank | $10.29B | Finance | Commercial Banks | | $WING | Wingstop Inc. | $875.69M | Consumer Services | Restaurants | | $ZF | Virtus Total Return Fund Inc. | $277.82M | n/a | n/a | import {Cell, Column, ColumnResizer, Group, ResizableTableContainer, Row, Table, TableBody, TableHeader} from 'react-aria-components'; import type {CellProps, ColumnProps, RowProps, SortDescriptor} from 'react-aria-components'; import ArrowUpIcon from '@spectrum-icons/ui/ArrowUpSmall'; import {useMemo, useState} from 'react'; function StockTableExample() { let [sortDescriptor, setSortDescriptor] = useState<SortDescriptor>({ column: 'symbol', direction: 'ascending' }); let sortedItems = useMemo(() => { return stocks.sort((a, b) => { let first = a[sortDescriptor.column]; let second = b[sortDescriptor.column]; let cmp = first.localeCompare(second); if (sortDescriptor.direction === 'descending') { cmp *= -1; } return cmp; }); }, [sortDescriptor]); return ( <div className="bg-linear-to-r from-indigo-500 to-violet-500 p-8 rounded-lg flex items-center justify-center md:col-span-2"> <ResizableTableContainer className="max-h-[280px] w-full overflow-auto scroll-pt-[2.321rem] relative bg-white rounded-lg shadow-sm text-gray-600"> <Table aria-label="Stocks" selectionMode="multiple" selectionBehavior="replace" sortDescriptor={sortDescriptor} onSortChange={setSortDescriptor} className="border-separate border-spacing-0" > <TableHeader> <StockColumn id="symbol" allowsSorting>Symbol</StockColumn> <StockColumn id="name" isRowHeader allowsSorting defaultWidth="3fr"> Name </StockColumn> <StockColumn id="marketCap" allowsSorting>Market Cap</StockColumn> <StockColumn id="sector" allowsSorting>Sector</StockColumn> <StockColumn id="industry" allowsSorting defaultWidth="2fr"> Industry </StockColumn> </TableHeader> <TableBody items={sortedItems}> {(item) => ( <StockRow> <StockCell> <span className="font-mono bg-slate-100 border border-slate-200 rounded-sm px-1 group-selected:bg-slate-700 group-selected:border-slate-800"> ${item.symbol} </span> </StockCell> <StockCell className="font-semibold">{item.name}</StockCell> <StockCell>{item.marketCap}</StockCell> <StockCell>{item.sector}</StockCell> <StockCell>{item.industry}</StockCell> </StockRow> )} </TableBody> </Table> </ResizableTableContainer> </div> ); } function StockColumn(props: ColumnProps & { children: React.ReactNode }) { return ( <Column {...props} className="sticky top-0 p-0 border-0 border-b border-solid border-slate-300 bg-slate-200 font-bold text-left cursor-default first:rounded-tl-lg last:rounded-tr-lg whitespace-nowrap outline-hidden" > {({ allowsSorting, sortDirection }) => ( <div className="flex items-center pl-4 py-1"> <Group role="presentation" tabIndex={-1} className="flex flex-1 items-center overflow-hidden outline-hidden rounded-sm focus-visible:ring-2 ring-slate-600" > <span className="flex-1 truncate">{props.children}</span> {allowsSorting && ( <span className={`ml-1 w-4 h-4 flex items-center justify-center transition ${ sortDirection === 'descending' ? 'rotate-180' : '' }`} > {sortDirection && <ArrowUpIcon width={8} height={10} />} </span> )} </Group> <ColumnResizer className="w-px px-[8px] py-1 h-5 bg-clip-content bg-slate-400 cursor-col-resize rounded-sm resizing:bg-slate-800 resizing:w-[2px] resizing:pl-[7px] focus-visible:ring-2 ring-slate-600 ring-inset" /> </div> )} </Column> ); } function StockRow<T extends object>(props: RowProps<T>) { return ( <Row {...props} className="even:bg-slate-100 selected:bg-slate-600 selected:text-white cursor-default group outline-hidden focus-visible:outline focus-visible:outline-2 focus-visible:outline-slate-600 focus-visible:-outline-offset-4 selected:focus-visible:outline-white" /> ); } function StockCell(props: CellProps) { return ( <Cell {...props} className={`px-4 py-2 truncate ${props.className} focus-visible:outline focus-visible:outline-2 focus-visible:outline-slate-600 focus-visible:-outline-offset-4 group-selected:focus-visible:outline-white`} /> ); } import { Cell, Column, ColumnResizer, Group, ResizableTableContainer, Row, Table, TableBody, TableHeader } from 'react-aria-components'; import type { CellProps, ColumnProps, RowProps, SortDescriptor } from 'react-aria-components'; import ArrowUpIcon from '@spectrum-icons/ui/ArrowUpSmall'; import {useMemo, useState} from 'react'; function StockTableExample() { let [sortDescriptor, setSortDescriptor] = useState< SortDescriptor >({ column: 'symbol', direction: 'ascending' }); let sortedItems = useMemo(() => { return stocks.sort((a, b) => { let first = a[sortDescriptor.column]; let second = b[sortDescriptor.column]; let cmp = first.localeCompare(second); if (sortDescriptor.direction === 'descending') { cmp *= -1; } return cmp; }); }, [sortDescriptor]); return ( <div className="bg-linear-to-r from-indigo-500 to-violet-500 p-8 rounded-lg flex items-center justify-center md:col-span-2"> <ResizableTableContainer className="max-h-[280px] w-full overflow-auto scroll-pt-[2.321rem] relative bg-white rounded-lg shadow-sm text-gray-600"> <Table aria-label="Stocks" selectionMode="multiple" selectionBehavior="replace" sortDescriptor={sortDescriptor} onSortChange={setSortDescriptor} className="border-separate border-spacing-0" > <TableHeader> <StockColumn id="symbol" allowsSorting> Symbol </StockColumn> <StockColumn id="name" isRowHeader allowsSorting defaultWidth="3fr" > Name </StockColumn> <StockColumn id="marketCap" allowsSorting> Market Cap </StockColumn> <StockColumn id="sector" allowsSorting> Sector </StockColumn> <StockColumn id="industry" allowsSorting defaultWidth="2fr" > Industry </StockColumn> </TableHeader> <TableBody items={sortedItems}> {(item) => ( <StockRow> <StockCell> <span className="font-mono bg-slate-100 border border-slate-200 rounded-sm px-1 group-selected:bg-slate-700 group-selected:border-slate-800"> ${item.symbol} </span> </StockCell> <StockCell className="font-semibold"> {item.name} </StockCell> <StockCell>{item.marketCap}</StockCell> <StockCell>{item.sector}</StockCell> <StockCell>{item.industry}</StockCell> </StockRow> )} </TableBody> </Table> </ResizableTableContainer> </div> ); } function StockColumn( props: ColumnProps & { children: React.ReactNode } ) { return ( <Column {...props} className="sticky top-0 p-0 border-0 border-b border-solid border-slate-300 bg-slate-200 font-bold text-left cursor-default first:rounded-tl-lg last:rounded-tr-lg whitespace-nowrap outline-hidden" > {({ allowsSorting, sortDirection }) => ( <div className="flex items-center pl-4 py-1"> <Group role="presentation" tabIndex={-1} className="flex flex-1 items-center overflow-hidden outline-hidden rounded-sm focus-visible:ring-2 ring-slate-600" > <span className="flex-1 truncate"> {props.children} </span> {allowsSorting && ( <span className={`ml-1 w-4 h-4 flex items-center justify-center transition ${ sortDirection === 'descending' ? 'rotate-180' : '' }`} > {sortDirection && ( <ArrowUpIcon width={8} height={10} /> )} </span> )} </Group> <ColumnResizer className="w-px px-[8px] py-1 h-5 bg-clip-content bg-slate-400 cursor-col-resize rounded-sm resizing:bg-slate-800 resizing:w-[2px] resizing:pl-[7px] focus-visible:ring-2 ring-slate-600 ring-inset" /> </div> )} </Column> ); } function StockRow<T extends object>(props: RowProps<T>) { return ( <Row {...props} className="even:bg-slate-100 selected:bg-slate-600 selected:text-white cursor-default group outline-hidden focus-visible:outline focus-visible:outline-2 focus-visible:outline-slate-600 focus-visible:-outline-offset-4 selected:focus-visible:outline-white" /> ); } function StockCell(props: CellProps) { return ( <Cell {...props} className={`px-4 py-2 truncate ${props.className} focus-visible:outline focus-visible:outline-2 focus-visible:outline-slate-600 focus-visible:-outline-offset-4 group-selected:focus-visible:outline-white`} /> ); } import { Cell, Column, ColumnResizer, Group, ResizableTableContainer, Row, Table, TableBody, TableHeader } from 'react-aria-components'; import type { CellProps, ColumnProps, RowProps, SortDescriptor } from 'react-aria-components'; import ArrowUpIcon from '@spectrum-icons/ui/ArrowUpSmall'; import { useMemo, useState } from 'react'; function StockTableExample() { let [ sortDescriptor, setSortDescriptor ] = useState< SortDescriptor >({ column: 'symbol', direction: 'ascending' }); let sortedItems = useMemo(() => { return stocks.sort( (a, b) => { let first = a[ sortDescriptor .column ]; let second = b[ sortDescriptor .column ]; let cmp = first .localeCompare( second ); if ( sortDescriptor .direction === 'descending' ) { cmp *= -1; } return cmp; } ); }, [sortDescriptor]); return ( <div className="bg-linear-to-r from-indigo-500 to-violet-500 p-8 rounded-lg flex items-center justify-center md:col-span-2"> <ResizableTableContainer className="max-h-[280px] w-full overflow-auto scroll-pt-[2.321rem] relative bg-white rounded-lg shadow-sm text-gray-600"> <Table aria-label="Stocks" selectionMode="multiple" selectionBehavior="replace" sortDescriptor={sortDescriptor} onSortChange={setSortDescriptor} className="border-separate border-spacing-0" > <TableHeader> <StockColumn id="symbol" allowsSorting > Symbol </StockColumn> <StockColumn id="name" isRowHeader allowsSorting defaultWidth="3fr" > Name </StockColumn> <StockColumn id="marketCap" allowsSorting > Market Cap </StockColumn> <StockColumn id="sector" allowsSorting > Sector </StockColumn> <StockColumn id="industry" allowsSorting defaultWidth="2fr" > Industry </StockColumn> </TableHeader> <TableBody items={sortedItems} > {(item) => ( <StockRow> <StockCell> <span className="font-mono bg-slate-100 border border-slate-200 rounded-sm px-1 group-selected:bg-slate-700 group-selected:border-slate-800"> ${item .symbol} </span> </StockCell> <StockCell className="font-semibold"> {item .name} </StockCell> <StockCell> {item .marketCap} </StockCell> <StockCell> {item .sector} </StockCell> <StockCell> {item .industry} </StockCell> </StockRow> )} </TableBody> </Table> </ResizableTableContainer> </div> ); } function StockColumn( props: ColumnProps & { children: React.ReactNode; } ) { return ( <Column {...props} className="sticky top-0 p-0 border-0 border-b border-solid border-slate-300 bg-slate-200 font-bold text-left cursor-default first:rounded-tl-lg last:rounded-tr-lg whitespace-nowrap outline-hidden" > {( { allowsSorting, sortDirection } ) => ( <div className="flex items-center pl-4 py-1"> <Group role="presentation" tabIndex={-1} className="flex flex-1 items-center overflow-hidden outline-hidden rounded-sm focus-visible:ring-2 ring-slate-600" > <span className="flex-1 truncate"> {props .children} </span> {allowsSorting && ( <span className={`ml-1 w-4 h-4 flex items-center justify-center transition ${ sortDirection === 'descending' ? 'rotate-180' : '' }`} > {sortDirection && ( <ArrowUpIcon width={8} height={10} /> )} </span> )} </Group> <ColumnResizer className="w-px px-[8px] py-1 h-5 bg-clip-content bg-slate-400 cursor-col-resize rounded-sm resizing:bg-slate-800 resizing:w-[2px] resizing:pl-[7px] focus-visible:ring-2 ring-slate-600 ring-inset" /> </div> )} </Column> ); } function StockRow< T extends object >(props: RowProps<T>) { return ( <Row {...props} className="even:bg-slate-100 selected:bg-slate-600 selected:text-white cursor-default group outline-hidden focus-visible:outline focus-visible:outline-2 focus-visible:outline-slate-600 focus-visible:-outline-offset-4 selected:focus-visible:outline-white" /> ); } function StockCell( props: CellProps ) { return ( <Cell {...props} className={`px-4 py-2 truncate ${props.className} focus-visible:outline focus-visible:outline-2 focus-visible:outline-slate-600 focus-visible:-outline-offset-4 group-selected:focus-visible:outline-white`} /> ); } ### Tailwind config# This example uses the tailwindcss-react-aria-components plugin. When using Tailwind v4, add it to your CSS: @import "tailwindcss"; @plugin "tailwindcss-react-aria-components"; @import "tailwindcss"; @plugin "tailwindcss-react-aria-components"; @import "tailwindcss"; @plugin "tailwindcss-react-aria-components"; Tailwind v3 When using Tailwind v3, add the plugin to your `tailwind.config.js` instead: module.exports = { // ... plugins: [ require('tailwindcss-react-aria-components') ] }; module.exports = { // ... plugins: [ require('tailwindcss-react-aria-components') ] }; module.exports = { // ... plugins: [ require( 'tailwindcss-react-aria-components' ) ] }; **Note**: When using Tailwind v3, install `tailwindcss-react-aria-components` version 1.x instead of 2.x. ## Components# * * * Table A table displays data in rows and columns, with row selection and sorting. --- ## Page: https://react-spectrum.adobe.com/react-aria/examples/swipeable-tabs.html React AriaExamples # Swipeable Tabs A swipeable Tabs component built with with React Aria Components, Framer Motion, Tailwind CSS, and CSS scroll snapping. ## Example# * * * World N.Y. Business Arts Science ## World contents... Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean sit amet nisl blandit, pellentesque eros eu, scelerisque eros. Sed cursus urna at nunc lacinia dapibus. ## N.Y. contents... Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean sit amet nisl blandit, pellentesque eros eu, scelerisque eros. Sed cursus urna at nunc lacinia dapibus. ## Business contents... Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean sit amet nisl blandit, pellentesque eros eu, scelerisque eros. Sed cursus urna at nunc lacinia dapibus. ## Arts contents... Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean sit amet nisl blandit, pellentesque eros eu, scelerisque eros. Sed cursus urna at nunc lacinia dapibus. ## Science contents... Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean sit amet nisl blandit, pellentesque eros eu, scelerisque eros. Sed cursus urna at nunc lacinia dapibus. import {Collection, Tab, TabList, TabPanel, Tabs} from 'react-aria-components'; import {animate, motion, useMotionValueEvent, useScroll, useTransform} from 'framer-motion'; import {useCallback, useEffect, useRef, useState} from 'react'; let tabs = [ { id: 'world', label: 'World' }, { id: 'ny', label: 'N.Y.' }, { id: 'business', label: 'Business' }, { id: 'arts', label: 'Arts' }, { id: 'science', label: 'Science' } ]; function AnimatedTabs() { let [selectedKey, setSelectedKey] = useState(tabs[0].id); let tabListRef = useRef(null); let tabPanelsRef = useRef(null); // Track the scroll position of the tab panel container. let { scrollXProgress } = useScroll({ container: tabPanelsRef }); // Find all the tab elements so we can use their dimensions. let [tabElements, setTabElements] = useState([]); useEffect(() => { if (tabElements.length === 0) { let tabs = tabListRef.current.querySelectorAll('[role=tab]'); setTabElements(tabs); } }, [tabElements]); // This function determines which tab should be selected // based on the scroll position. let getIndex = useCallback( (x) => Math.max(0, Math.floor((tabElements.length - 1) * x)), [tabElements] ); // This function transforms the scroll position into the X position // or width of the selected tab indicator. let transform = (x, property) => { if (!tabElements.length) return 0; // Find the tab index for the scroll X position. let index = getIndex(x); // Get the difference between this tab and the next one. let difference = index < tabElements.length - 1 ? tabElements[index + 1][property] - tabElements[index][property] : tabElements[index].offsetWidth; // Get the percentage between tabs. // This is the difference between the integer index and fractional one. let percent = (tabElements.length - 1) * x - index; // Linearly interpolate to calculate the position of the selection indicator. let value = tabElements[index][property] + difference * percent; // iOS scrolls weird when translateX is 0 for some reason. 🤷♂️ return value || 0.1; }; let x = useTransform(scrollXProgress, (x) => transform(x, 'offsetLeft')); let width = useTransform(scrollXProgress, (x) => transform(x, 'offsetWidth')); // When the user scrolls, update the selected key // so that the correct tab panel becomes interactive. useMotionValueEvent(scrollXProgress, 'change', (x) => { if (animationRef.current || !tabElements.length) return; setSelectedKey(tabs[getIndex(x)].id); }); // When the user clicks on a tab perform an animation of // the scroll position to the newly selected tab panel. let animationRef = useRef(null); let onSelectionChange = (selectedKey) => { setSelectedKey(selectedKey); // If the scroll position is already moving but we aren't animating // then the key changed as a result of a user scrolling. Ignore. if (scrollXProgress.getVelocity() && !animationRef.current) { return; } let tabPanel = tabPanelsRef.current; let index = tabs.findIndex((tab) => tab.id === selectedKey); animationRef.current?.stop(); animationRef.current = animate( tabPanel.scrollLeft, tabPanel.scrollWidth * (index / tabs.length), { type: 'spring', bounce: 0.2, duration: 0.6, onUpdate: (v) => { tabPanel.scrollLeft = v; }, onPlay: () => { // Disable scroll snap while the animation is going or weird things happen. tabPanel.style.scrollSnapType = 'none'; }, onComplete: () => { tabPanel.style.scrollSnapType = ''; animationRef.current = null; } } ); }; return ( <Tabs className="w-fit max-w-[min(100%,350px)]" selectedKey={selectedKey} onSelectionChange={onSelectionChange} > <div className="relative"> <TabList ref={tabListRef} className="flex -mx-1" items={tabs}> {(tab) => ( <Tab className="cursor-default px-3 py-1.5 text-md transition outline-hidden touch-none"> {({ isSelected, isFocusVisible }) => ( <> {tab.label} {isFocusVisible && isSelected && ( // Focus ring. <motion.span className="absolute inset-0 z-10 rounded-full ring-2 ring-black ring-offset-2" style={{ x, width }} /> )} </> )} </Tab> )} </TabList> {/* Selection indicator. */} <motion.span className="absolute inset-0 z-10 bg-white rounded-full mix-blend-difference" style={{ x, width }} /> </div> <div ref={tabPanelsRef} className="my-4 overflow-auto snap-x snap-mandatory no-scrollbar flex" > <Collection items={tabs}> {(tab) => ( <TabPanel shouldForceMount className="shrink-0 w-full px-2 box-border snap-start outline-hidden -outline-offset-2 rounded-sm focus-visible:outline-black" > <h2>{tab.label} contents...</h2> <p> Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean sit amet nisl blandit, pellentesque eros eu, scelerisque eros. Sed cursus urna at nunc lacinia dapibus. </p> </TabPanel> )} </Collection> </div> </Tabs> ); } import { Collection, Tab, TabList, TabPanel, Tabs } from 'react-aria-components'; import { animate, motion, useMotionValueEvent, useScroll, useTransform } from 'framer-motion'; import { useCallback, useEffect, useRef, useState } from 'react'; let tabs = [ { id: 'world', label: 'World' }, { id: 'ny', label: 'N.Y.' }, { id: 'business', label: 'Business' }, { id: 'arts', label: 'Arts' }, { id: 'science', label: 'Science' } ]; function AnimatedTabs() { let [selectedKey, setSelectedKey] = useState(tabs[0].id); let tabListRef = useRef(null); let tabPanelsRef = useRef(null); // Track the scroll position of the tab panel container. let { scrollXProgress } = useScroll({ container: tabPanelsRef }); // Find all the tab elements so we can use their dimensions. let [tabElements, setTabElements] = useState([]); useEffect(() => { if (tabElements.length === 0) { let tabs = tabListRef.current.querySelectorAll( '[role=tab]' ); setTabElements(tabs); } }, [tabElements]); // This function determines which tab should be selected // based on the scroll position. let getIndex = useCallback( (x) => Math.max(0, Math.floor((tabElements.length - 1) * x)), [tabElements] ); // This function transforms the scroll position into the X position // or width of the selected tab indicator. let transform = (x, property) => { if (!tabElements.length) return 0; // Find the tab index for the scroll X position. let index = getIndex(x); // Get the difference between this tab and the next one. let difference = index < tabElements.length - 1 ? tabElements[index + 1][property] - tabElements[index][property] : tabElements[index].offsetWidth; // Get the percentage between tabs. // This is the difference between the integer index and fractional one. let percent = (tabElements.length - 1) * x - index; // Linearly interpolate to calculate the position of the selection indicator. let value = tabElements[index][property] + difference * percent; // iOS scrolls weird when translateX is 0 for some reason. 🤷♂️ return value || 0.1; }; let x = useTransform( scrollXProgress, (x) => transform(x, 'offsetLeft') ); let width = useTransform( scrollXProgress, (x) => transform(x, 'offsetWidth') ); // When the user scrolls, update the selected key // so that the correct tab panel becomes interactive. useMotionValueEvent(scrollXProgress, 'change', (x) => { if (animationRef.current || !tabElements.length) return; setSelectedKey(tabs[getIndex(x)].id); }); // When the user clicks on a tab perform an animation of // the scroll position to the newly selected tab panel. let animationRef = useRef(null); let onSelectionChange = (selectedKey) => { setSelectedKey(selectedKey); // If the scroll position is already moving but we aren't animating // then the key changed as a result of a user scrolling. Ignore. if ( scrollXProgress.getVelocity() && !animationRef.current ) { return; } let tabPanel = tabPanelsRef.current; let index = tabs.findIndex((tab) => tab.id === selectedKey ); animationRef.current?.stop(); animationRef.current = animate( tabPanel.scrollLeft, tabPanel.scrollWidth * (index / tabs.length), { type: 'spring', bounce: 0.2, duration: 0.6, onUpdate: (v) => { tabPanel.scrollLeft = v; }, onPlay: () => { // Disable scroll snap while the animation is going or weird things happen. tabPanel.style.scrollSnapType = 'none'; }, onComplete: () => { tabPanel.style.scrollSnapType = ''; animationRef.current = null; } } ); }; return ( <Tabs className="w-fit max-w-[min(100%,350px)]" selectedKey={selectedKey} onSelectionChange={onSelectionChange} > <div className="relative"> <TabList ref={tabListRef} className="flex -mx-1" items={tabs} > {(tab) => ( <Tab className="cursor-default px-3 py-1.5 text-md transition outline-hidden touch-none"> {({ isSelected, isFocusVisible }) => ( <> {tab.label} {isFocusVisible && isSelected && ( // Focus ring. <motion.span className="absolute inset-0 z-10 rounded-full ring-2 ring-black ring-offset-2" style={{ x, width }} /> )} </> )} </Tab> )} </TabList> {/* Selection indicator. */} <motion.span className="absolute inset-0 z-10 bg-white rounded-full mix-blend-difference" style={{ x, width }} /> </div> <div ref={tabPanelsRef} className="my-4 overflow-auto snap-x snap-mandatory no-scrollbar flex" > <Collection items={tabs}> {(tab) => ( <TabPanel shouldForceMount className="shrink-0 w-full px-2 box-border snap-start outline-hidden -outline-offset-2 rounded-sm focus-visible:outline-black" > <h2>{tab.label} contents...</h2> <p> Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean sit amet nisl blandit, pellentesque eros eu, scelerisque eros. Sed cursus urna at nunc lacinia dapibus. </p> </TabPanel> )} </Collection> </div> </Tabs> ); } import { Collection, Tab, TabList, TabPanel, Tabs } from 'react-aria-components'; import { animate, motion, useMotionValueEvent, useScroll, useTransform } from 'framer-motion'; import { useCallback, useEffect, useRef, useState } from 'react'; let tabs = [ { id: 'world', label: 'World' }, { id: 'ny', label: 'N.Y.' }, { id: 'business', label: 'Business' }, { id: 'arts', label: 'Arts' }, { id: 'science', label: 'Science' } ]; function AnimatedTabs() { let [ selectedKey, setSelectedKey ] = useState( tabs[0].id ); let tabListRef = useRef(null); let tabPanelsRef = useRef(null); // Track the scroll position of the tab panel container. let { scrollXProgress } = useScroll({ container: tabPanelsRef }); // Find all the tab elements so we can use their dimensions. let [ tabElements, setTabElements ] = useState([]); useEffect(() => { if ( tabElements .length === 0 ) { let tabs = tabListRef .current .querySelectorAll( '[role=tab]' ); setTabElements( tabs ); } }, [tabElements]); // This function determines which tab should be selected // based on the scroll position. let getIndex = useCallback( (x) => Math.max( 0, Math.floor( (tabElements .length - 1) * x ) ), [tabElements] ); // This function transforms the scroll position into the X position // or width of the selected tab indicator. let transform = ( x, property ) => { if ( !tabElements.length ) return 0; // Find the tab index for the scroll X position. let index = getIndex( x ); // Get the difference between this tab and the next one. let difference = index < tabElements .length - 1 ? tabElements[ index + 1 ][property] - tabElements[ index ][property] : tabElements[ index ].offsetWidth; // Get the percentage between tabs. // This is the difference between the integer index and fractional one. let percent = (tabElements .length - 1) * x - index; // Linearly interpolate to calculate the position of the selection indicator. let value = tabElements[index][ property ] + difference * percent; // iOS scrolls weird when translateX is 0 for some reason. 🤷♂️ return value || 0.1; }; let x = useTransform( scrollXProgress, (x) => transform( x, 'offsetLeft' ) ); let width = useTransform( scrollXProgress, (x) => transform( x, 'offsetWidth' ) ); // When the user scrolls, update the selected key // so that the correct tab panel becomes interactive. useMotionValueEvent( scrollXProgress, 'change', (x) => { if ( animationRef .current || !tabElements .length ) return; setSelectedKey( tabs[getIndex(x)] .id ); } ); // When the user clicks on a tab perform an animation of // the scroll position to the newly selected tab panel. let animationRef = useRef(null); let onSelectionChange = (selectedKey) => { setSelectedKey( selectedKey ); // If the scroll position is already moving but we aren't animating // then the key changed as a result of a user scrolling. Ignore. if ( scrollXProgress .getVelocity() && !animationRef .current ) { return; } let tabPanel = tabPanelsRef .current; let index = tabs .findIndex(( tab ) => tab.id === selectedKey ); animationRef .current?.stop(); animationRef .current = animate( tabPanel .scrollLeft, tabPanel .scrollWidth * (index / tabs .length), { type: 'spring', bounce: 0.2, duration: 0.6, onUpdate: ( v ) => { tabPanel .scrollLeft = v; }, onPlay: () => { // Disable scroll snap while the animation is going or weird things happen. tabPanel .style .scrollSnapType = 'none'; }, onComplete: () => { tabPanel .style .scrollSnapType = ''; animationRef .current = null; } } ); }; return ( <Tabs className="w-fit max-w-[min(100%,350px)]" selectedKey={selectedKey} onSelectionChange={onSelectionChange} > <div className="relative"> <TabList ref={tabListRef} className="flex -mx-1" items={tabs} > {(tab) => ( <Tab className="cursor-default px-3 py-1.5 text-md transition outline-hidden touch-none"> {( { isSelected, isFocusVisible } ) => ( <> {tab .label} {isFocusVisible && isSelected && ( // Focus ring. <motion.span className="absolute inset-0 z-10 rounded-full ring-2 ring-black ring-offset-2" style={{ x, width }} /> )} </> )} </Tab> )} </TabList> {/* Selection indicator. */} <motion.span className="absolute inset-0 z-10 bg-white rounded-full mix-blend-difference" style={{ x, width }} /> </div> <div ref={tabPanelsRef} className="my-4 overflow-auto snap-x snap-mandatory no-scrollbar flex" > <Collection items={tabs} > {(tab) => ( <TabPanel shouldForceMount className="shrink-0 w-full px-2 box-border snap-start outline-hidden -outline-offset-2 rounded-sm focus-visible:outline-black" > <h2> {tab .label} {' '} contents... </h2> <p> Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean sit amet nisl blandit, pellentesque eros eu, scelerisque eros. Sed cursus urna at nunc lacinia dapibus. </p> </TabPanel> )} </Collection> </div> </Tabs> ); } ### Tailwind config# This example uses the tailwindcss-react-aria-components plugin. When using Tailwind v4, add it to your CSS: @import "tailwindcss"; @plugin "tailwindcss-react-aria-components"; @import "tailwindcss"; @plugin "tailwindcss-react-aria-components"; @import "tailwindcss"; @plugin "tailwindcss-react-aria-components"; Tailwind v3 When using Tailwind v3, add the plugin to your `tailwind.config.js` instead: module.exports = { // ... plugins: [ require('tailwindcss-react-aria-components') ] }; module.exports = { // ... plugins: [ require('tailwindcss-react-aria-components') ] }; module.exports = { // ... plugins: [ require( 'tailwindcss-react-aria-components' ) ] }; **Note**: When using Tailwind v3, install `tailwindcss-react-aria-components` version 1.x instead of 2.x. ## Components# * * * Tabs Tabs organize content into multiple sections, and allow a user to view one at a time. ## Learn more# * * * This example was inspired by Sam Selikoff's video "Animated tabs with inverted text". Check it out to learn more about how the inverted text effect works! --- ## Page: https://react-spectrum.adobe.com/react-aria/examples/user-combobox.html React AriaExamples # User Search ComboBox A user search ComboBox styled with Tailwind CSS. ## Example# * * * Assignee import {Button, ComboBox, Group, Input, Label, ListBox, ListBoxItem, Popover} from 'react-aria-components'; import type {ListBoxItemProps} from 'react-aria-components'; import ChevronUpDownIcon from '@spectrum-icons/workflow/ChevronUpDown'; import CheckIcon from '@spectrum-icons/workflow/Checkmark'; function ComboBoxExample() { return ( <div className="bg-linear-to-r from-sky-300 to-cyan-300 p-8 sm:h-[300px] rounded-lg flex justify-center"> <ComboBox className="group flex flex-col gap-1 w-[200px]"> <Label className="text-black cursor-default">Assignee</Label> <Group className="flex rounded-lg bg-white/90 focus-within:bg-white transition shadow-md ring-1 ring-black/10 focus-visible:ring-2 focus-visible:ring-black"> <Input className="flex-1 w-full border-none py-2 px-3 leading-5 text-gray-900 bg-transparent outline-hidden text-base" /> <Button className="px-3 flex items-center text-gray-700 transition border-0 border-solid border-l border-l-sky-200 bg-transparent rounded-r-lg pressed:bg-sky-100"> <ChevronUpDownIcon size="XS" /> </Button> </Group> <Popover className="max-h-60 w-(--trigger-width) overflow-auto rounded-md bg-white text-base shadow-lg ring-1 ring-black/5 entering:animate-in entering:fade-in exiting:animate-out exiting:fade-out"> <ListBox className="outline-hidden p-1" items={people}> {(item) => ( <UserItem textValue={item.name}> <img alt="" src={item.avatar} className="w-6 h-6 rounded-full" /> <span className="truncate">{item.name}</span> </UserItem> )} </ListBox> </Popover> </ComboBox> </div> ); } function UserItem(props: ListBoxItemProps & { children: React.ReactNode }) { return ( <ListBoxItem {...props} className="group flex items-center gap-2 cursor-default select-none py-2 pl-2 pr-4 outline-hidden rounded-sm text-gray-900 focus:bg-sky-600 focus:text-white" > {({ isSelected }) => ( <> <span className="flex-1 flex items-center gap-3 truncate font-normal group-selected:font-medium"> {props.children} </span> {isSelected && ( <span className="w-5 flex items-center text-sky-600 group-focus:text-white"> <CheckIcon size="S" /> </span> )} </> )} </ListBoxItem> ); } import { Button, ComboBox, Group, Input, Label, ListBox, ListBoxItem, Popover } from 'react-aria-components'; import type {ListBoxItemProps} from 'react-aria-components'; import ChevronUpDownIcon from '@spectrum-icons/workflow/ChevronUpDown'; import CheckIcon from '@spectrum-icons/workflow/Checkmark'; function ComboBoxExample() { return ( <div className="bg-linear-to-r from-sky-300 to-cyan-300 p-8 sm:h-[300px] rounded-lg flex justify-center"> <ComboBox className="group flex flex-col gap-1 w-[200px]"> <Label className="text-black cursor-default"> Assignee </Label> <Group className="flex rounded-lg bg-white/90 focus-within:bg-white transition shadow-md ring-1 ring-black/10 focus-visible:ring-2 focus-visible:ring-black"> <Input className="flex-1 w-full border-none py-2 px-3 leading-5 text-gray-900 bg-transparent outline-hidden text-base" /> <Button className="px-3 flex items-center text-gray-700 transition border-0 border-solid border-l border-l-sky-200 bg-transparent rounded-r-lg pressed:bg-sky-100"> <ChevronUpDownIcon size="XS" /> </Button> </Group> <Popover className="max-h-60 w-(--trigger-width) overflow-auto rounded-md bg-white text-base shadow-lg ring-1 ring-black/5 entering:animate-in entering:fade-in exiting:animate-out exiting:fade-out"> <ListBox className="outline-hidden p-1" items={people} > {(item) => ( <UserItem textValue={item.name}> <img alt="" src={item.avatar} className="w-6 h-6 rounded-full" /> <span className="truncate"> {item.name} </span> </UserItem> )} </ListBox> </Popover> </ComboBox> </div> ); } function UserItem( props: ListBoxItemProps & { children: React.ReactNode } ) { return ( <ListBoxItem {...props} className="group flex items-center gap-2 cursor-default select-none py-2 pl-2 pr-4 outline-hidden rounded-sm text-gray-900 focus:bg-sky-600 focus:text-white" > {({ isSelected }) => ( <> <span className="flex-1 flex items-center gap-3 truncate font-normal group-selected:font-medium"> {props.children} </span> {isSelected && ( <span className="w-5 flex items-center text-sky-600 group-focus:text-white"> <CheckIcon size="S" /> </span> )} </> )} </ListBoxItem> ); } import { Button, ComboBox, Group, Input, Label, ListBox, ListBoxItem, Popover } from 'react-aria-components'; import type {ListBoxItemProps} from 'react-aria-components'; import ChevronUpDownIcon from '@spectrum-icons/workflow/ChevronUpDown'; import CheckIcon from '@spectrum-icons/workflow/Checkmark'; function ComboBoxExample() { return ( <div className="bg-linear-to-r from-sky-300 to-cyan-300 p-8 sm:h-[300px] rounded-lg flex justify-center"> <ComboBox className="group flex flex-col gap-1 w-[200px]"> <Label className="text-black cursor-default"> Assignee </Label> <Group className="flex rounded-lg bg-white/90 focus-within:bg-white transition shadow-md ring-1 ring-black/10 focus-visible:ring-2 focus-visible:ring-black"> <Input className="flex-1 w-full border-none py-2 px-3 leading-5 text-gray-900 bg-transparent outline-hidden text-base" /> <Button className="px-3 flex items-center text-gray-700 transition border-0 border-solid border-l border-l-sky-200 bg-transparent rounded-r-lg pressed:bg-sky-100"> <ChevronUpDownIcon size="XS" /> </Button> </Group> <Popover className="max-h-60 w-(--trigger-width) overflow-auto rounded-md bg-white text-base shadow-lg ring-1 ring-black/5 entering:animate-in entering:fade-in exiting:animate-out exiting:fade-out"> <ListBox className="outline-hidden p-1" items={people} > {(item) => ( <UserItem textValue={item .name} > <img alt="" src={item .avatar} className="w-6 h-6 rounded-full" /> <span className="truncate"> {item .name} </span> </UserItem> )} </ListBox> </Popover> </ComboBox> </div> ); } function UserItem( props: & ListBoxItemProps & { children: React.ReactNode; } ) { return ( <ListBoxItem {...props} className="group flex items-center gap-2 cursor-default select-none py-2 pl-2 pr-4 outline-hidden rounded-sm text-gray-900 focus:bg-sky-600 focus:text-white" > {( { isSelected } ) => ( <> <span className="flex-1 flex items-center gap-3 truncate font-normal group-selected:font-medium"> {props .children} </span> {isSelected && ( <span className="w-5 flex items-center text-sky-600 group-focus:text-white"> <CheckIcon size="S" /> </span> )} </> )} </ListBoxItem> ); } ### Tailwind config# This example uses the following plugins: * tailwindcss-react-aria-components * tailwindcss-animate When using Tailwind v4, add them to your CSS: @import "tailwindcss"; @plugin "tailwindcss-react-aria-components"; @plugin "tailwindcss-animate"; @import "tailwindcss"; @plugin "tailwindcss-react-aria-components"; @plugin "tailwindcss-animate"; @import "tailwindcss"; @plugin "tailwindcss-react-aria-components"; @plugin "tailwindcss-animate"; Tailwind v3 When using Tailwind v3, add the plugins to your `tailwind.config.js` instead: module.exports = { // ... plugins: [ require('tailwindcss-react-aria-components'), require('tailwindcss-animate') ] }; module.exports = { // ... plugins: [ require('tailwindcss-react-aria-components'), require('tailwindcss-animate') ] }; module.exports = { // ... plugins: [ require( 'tailwindcss-react-aria-components' ), require( 'tailwindcss-animate' ) ] }; **Note**: When using Tailwind v3, install `tailwindcss-react-aria-components` version 1.x instead of 2.x. ## Components# * * * ComboBox A combobox combines a text input with a listbox, and allows a user to filter a list of options. ListBox A listbox allows a user to select one or more options from a list. Popover A popover displays content in context with a trigger element. Button A button allows a user to perform an action. --- ## Page: https://react-spectrum.adobe.com/react-aria/examples/wifi-switch.html React AriaExamples # Wi-Fi Switch An animated Wi-Fi Switch styled with Tailwind CSS. ## Example# * * * Wi-Fi import {Switch} from 'react-aria-components'; <div className="bg-linear-to-r from-yellow-300 to-orange-300 p-12 rounded-lg flex justify-center"> <Switch className="group relative flex gap-2 items-center text-black font-semibold text-lg"> <div className="flex h-[26px] w-[44px] shrink-0 cursor-default rounded-full shadow-inner bg-clip-padding border border-solid border-white/30 p-[3px] box-border transition duration-200 ease-in-out bg-yellow-600 group-pressed:bg-yellow-700 group-selected:bg-amber-800 group-selected:group-pressed:bg-amber-900 outline-hidden group-focus-visible:ring-2 ring-black"> <span className="h-[18px] w-[18px] transform rounded-full bg-white shadow-sm transition duration-200 ease-in-out translate-x-0 group-selected:translate-x-[100%]" /> </div> Wi-Fi </Switch> </div> import {Switch} from 'react-aria-components'; <div className="bg-linear-to-r from-yellow-300 to-orange-300 p-12 rounded-lg flex justify-center"> <Switch className="group relative flex gap-2 items-center text-black font-semibold text-lg"> <div className="flex h-[26px] w-[44px] shrink-0 cursor-default rounded-full shadow-inner bg-clip-padding border border-solid border-white/30 p-[3px] box-border transition duration-200 ease-in-out bg-yellow-600 group-pressed:bg-yellow-700 group-selected:bg-amber-800 group-selected:group-pressed:bg-amber-900 outline-hidden group-focus-visible:ring-2 ring-black"> <span className="h-[18px] w-[18px] transform rounded-full bg-white shadow-sm transition duration-200 ease-in-out translate-x-0 group-selected:translate-x-[100%]" /> </div> Wi-Fi </Switch> </div> import {Switch} from 'react-aria-components'; <div className="bg-linear-to-r from-yellow-300 to-orange-300 p-12 rounded-lg flex justify-center"> <Switch className="group relative flex gap-2 items-center text-black font-semibold text-lg"> <div className="flex h-[26px] w-[44px] shrink-0 cursor-default rounded-full shadow-inner bg-clip-padding border border-solid border-white/30 p-[3px] box-border transition duration-200 ease-in-out bg-yellow-600 group-pressed:bg-yellow-700 group-selected:bg-amber-800 group-selected:group-pressed:bg-amber-900 outline-hidden group-focus-visible:ring-2 ring-black"> <span className="h-[18px] w-[18px] transform rounded-full bg-white shadow-sm transition duration-200 ease-in-out translate-x-0 group-selected:translate-x-[100%]" /> </div> Wi-Fi </Switch> </div> ### Tailwind config# This example uses the tailwindcss-react-aria-components plugin. When using Tailwind v4, add it to your CSS: @import "tailwindcss"; @plugin "tailwindcss-react-aria-components"; @import "tailwindcss"; @plugin "tailwindcss-react-aria-components"; @import "tailwindcss"; @plugin "tailwindcss-react-aria-components"; Tailwind v3 When using Tailwind v3, add the plugin to your `tailwind.config.js` instead: module.exports = { // ... plugins: [ require('tailwindcss-react-aria-components') ] }; module.exports = { // ... plugins: [ require('tailwindcss-react-aria-components') ] }; module.exports = { // ... plugins: [ require( 'tailwindcss-react-aria-components' ) ] }; **Note**: When using Tailwind v3, install `tailwindcss-react-aria-components` version 1.x instead of 2.x. ## Components# * * * Switch A switch allows a user to turn a setting on or off. --- ## Page: https://react-spectrum.adobe.com/react-aria/react-aria-test-utils ### Error 404: Page not found This page isn't available. Try checking the URL or visit a different page. --- ## Page: https://react-spectrum.adobe.com/react-aria/examples/ # Examples Techniques for styling and animating React Aria Components, and integrating with other libraries. Account Menu A Menu with an interactive header, built with a Dialog and Popover. Action Menu An animated menu of actions, styled with Tailwind CSS. Category Tabs An article category tabs component styled with Tailwind CSS. Command Palette A command palette with actions, styled with Tailwind CSS. Contact List A ListBox featuring sticky section headers and multiple selection. DatePicker A DatePicker component styled with Tailwind CSS. Destructive Alert Dialog An animated confirmation dialog, styled with Tailwind CSS. File System Tree A tree with multiple selection and nested items. Gesture Driven Modal Sheet An iOS-style gesture driven modal sheet built with Framer Motion. Image Grid An async image gallery with selectable items, styled with Tailwind CSS. iOS List View A GridList with swipe gestures, layout animations, and multi selection. Loading ProgressBar A loading ProgressBar styled with Tailwind CSS. Notifications Popover A notifications popover styled with Tailwind CSS. Opacity Slider An opacity slider styled with Tailwind CSS. Ripple Button A button with an animated ripple effect styled with Tailwind CSS. Searchable Select A Select component with Autocomplete filtering. Shipping Radio Group A shipping options RadioGroup styled with Tailwind CSS. Status Select An issue status dropdown styled with Tailwind CSS. Stock Table A table with sticky headers, sorting, multiple selection, and column resizing. Swipeable Tabs A swipeable Tabs component built with Framer Motion and CSS scroll snapping. User Search ComboBox A user search ComboBox styled with Tailwind CSS. Wi-Fi Switch An animated Wi-Fi Switch styled with Tailwind CSS.