How to build your own React component with React Aria

This tutorial we'll walk you through how to use React Aria to build a web application in a Refine application. You'll create components using React Aria and use them to build a demo application.

What we'll cover

Steps we'll cover includes:

  • What is React Aria
  • Why use React Aria
  • Project Setup
  • Creating React Components
    • Create Button Component
    • Create Input Component
    • Create Header Component
    • Create Modal Component
  • Using React Aria Components
  • Adding Server side rendering
  • Conclusion

What is React Aria

React Aria is a set of React Hooks providing accessible UI primitives tailored for integration into your design system. It furnishes accessibility features and default behaviors for numerous common UI components, allowing you to focus on crafting a unique and compelling design. The library supports adaptive interactions, encompassing keyboard, mouse, touch, and screen reader inputs, ensuring an optimal user experience for all.

Why use React Aria

Here are some of the reasons you should consider using React Aria in your web application

Accessible

React Aria ensures seamless accessibility with comprehensive support for screen readers and keyboard navigation. It aligns with WAI-ARIA Authoring Practices, guaranteeing adherence to accessibility standards. Extensive testing has been done across various screen readers and devices for each component to optimize the user experience.

Adaptability

Irrespective of the UI complexity, React Aria maintains consistent functionality across mouse, touch, keyboard, and screen reader interactions. Thorough testing has been carried out on diverse browsers, devices, and platforms to ensure robust adaptability.

International

React Aria supports for over 30 languages, incorporating features like right-to-left behavior and internationalized date and number formatting. This makes it a versatile solution for catering to a global audience with diverse language and formatting requirements.

Customizable

React Aria does not impose any rendering, DOM structure, style approach, or design-specific information. It empowers users to focus on their design by providing interactions, behavior, and accessibility features. This approach allows for full customization while ensuring a streamlined development process.

Project setup

Let's proceed to initialize aRefine project for the creation of a React Aria-based component library. Execute the following command to set up the project.

npx superplate-cli -p refine-react component-library

The above command will prompt you to complete options for your project. Your selection should look like the screenshot below.

Then wait while Refine installs the required packages for this project. Once that is done, let's install React Aria. React Aria published all components as a separate module for adoptable sake, so you can choose to install the components independently or as all the package. Eg npm install @react-aria/button

To save our time, we'll install all the component which is packaged under the @react-aria scope with the command below.

yarn add react-aria

Now change the directory to the project folder and run the application with the command below.

cd component-library &&  yarn dev

Creating React Components

We'll use the React hooks provided by Aria to create component libraries for our Refine application. React Aria offers accessibility and behavior for React hooks. You must define the DOM structure for your component and send the DOM props supplied by each React Aria hook to the proper elements because it does not provide any rendering. Additionally, it allows you total control over the DOM by enabling you to add additional components for styling or layout control, such as CSS classes, inline styles, CSS-in-JS, etc. We'll explore some of them and how they work. To get started, create a component folder in the src folder of our React project to save the component libraries.

mkdir src/components

Create Button component

Let's start with the button component and we'll use the useButton hook. The useButton hook takes care of several cross-browser discrepancies in interactions and accessibility capabilities so you can focus on the styling. Create a Button.tsx in the component folder and add the code snippet below.

import React, { ElementType, RefObject } from "react";
import { useButton } from "@react-aria/button";
import { AriaButtonProps } from "react-aria";

export default function Button(props: AriaButtonProps<ElementType> | any) {
  let ref: RefObject<any> = React.useRef();
  let { buttonProps } = useButton(props, ref);

  return (
    <button {...buttonProps} ref={ref} style={props.style}>
      {props.children}
    </button>
  );
}

In the above code snippet, we imported the useButton hook and called it passing the props along with a ref to the DOM node for this component to get the buttonProp property. Then we spread the props returned from the hook into the button element that we want to render, passing the ref and the style props.

Now you can import and use this component, pass in the props you want, and add styles and event listeners.

<Button style={{backgroundColor:"red"}} onPress={()=>console.log('button clicked')}>click me</Button>

Create Input Component

Next, let's create the input component library using the useFocus and useTextField hooks. The useTextField hook offers a text field's behavior and accessibility implementation, while the useFocus hook handles focus events for the current target while ignoring events on the children of the focus element. Create an Input.tsx file in the component folder and add the code snippet below.

import React, { RefObject } from "react";
import { useFocus } from "@react-aria/interactions";
import { useTextField } from "@react-aria/textfield";
import { FocusProps } from "react-aria";

export default function Input(props: FocusProps| any) {
    let [value, setValue] = React.useState("");
    let { focusProps } = useFocus({
        onFocus: (e) => setValue((e.target as HTMLInputElement).value),
        onBlur: (e) => setValue((e.target as HTMLInputElement).value),
    });
    let { label } = props;
    let ref: RefObject<any> = React.useRef();
    let { labelProps, inputProps, errorMessageProps } =
        useTextField(props, ref);

  return (
    <div style={props.style}>
      <label {...labelProps}>{label}</label>
      <input {...inputProps} {...focusProps} ref={ref} />
      {value.length < 1 && (
        <div {...errorMessageProps} style={{ color: "red", fontSize: 12 }}>
          All fields are required
        </div>
      )}
    </div>
  );
}

In the above code snippet, we imported the two Aria hooks, we created setValue state to get the value in the input field. The setValue state will enable users to know if the user has fielded the input field. Then we called the useFocus hook which takes an object of event listeners as parameters. useFocus hook supports three event handlers.

  • onFocus: This handler is called when the element receives focus.
  • onBlur: This handler is called when the element loses focus.
  • onFocusChange: This handler is called when the element's focus status changes.

We only need the onFocus and onBlur events to track the state of the input field when a user clicks in and out of it. Also, we called the useTextField, passing the props along with a ref to the DOM node for this component to get the labelProps, inputProps, and errorMessageProps properties. Then we spread the props returned from the hook into the input element that we want to render, passing the ref and the style props.

ThelabelProps, inputProps, and errorMessageProps handle the behavior of the input label, error message, and input properties.

Create Header Component

To create a Header component we'll use the useHover hook. This hook handles the pointer hover interactions for an element. Create a Header.tsx file in the component directory and add the code snippet below.

import React, { ElementType } from "react";
import { useHover } from "@react-aria/interactions";
import { AriaButtonProps } from "react-aria";

export default function Header(props: AriaButtonProps<ElementType>| any) {
  let { hoverProps, isHovered } = useHover({});

  return (
    <div
      {...hoverProps}
      style={{
        background: isHovered ? "#167B73" : "#2D9E96",
        color: "white",
        padding: 4,
        cursor: "pointer",
        display: "block",
      }}
      tabIndex={0}
    >
      <div style={{display:"flex", justifyContent:"space-between", fontSize:"10px"}}>{props.children}</div>
    </div>
  );
}

In the above code snippet, we imported the two Aria useFocus hooks. We called it to get the hoverProps and isHoverd properties. We'll use the isHovered props to know when the mouse or pen goes over the element and we change the background color of the elements in the header. Then we spread the hoverProps into the div element that we want to render, passing an initial tabIndex of 0.

Create Modal Component

We'll take advantage of the useDialog, useFocusScope, useOverly, useOverlyPreventScroll, useModal, useOverlayContainer and useButton hooks. Create a Model.tsx file in the component folder and add the code snippet below.

import React, { ElementType, RefObject } from 'react';
import { useOverlayTriggerState } from '@react-stately/overlays';
import {
  useOverlay,
  usePreventScroll,
  useModal,
  OverlayContainer
} from '@react-aria/overlays';
import { useDialog } from '@react-aria/dialog';
import { FocusScope } from '@react-aria/focus';
import { useButton } from '@react-aria/button';
import { AriaButtonProps, OverlayProvider } from 'react-aria';
import Button from './Button';

function ModalDialog(props: AriaButtonProps<ElementType> | any) {
  let { title, children } = props;
  let ref: RefObject<any> = React.useRef();
  let { overlayProps, underlayProps } = useOverlay(props, ref);

  usePreventScroll();
  let { modalProps } = useModal();
  let { dialogProps, titleProps } = useDialog(props, ref);

  return (
    <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}>
      <FocusScope contain restoreFocus autoFocus>
        <div
          {...overlayProps}
          {...dialogProps}
          {...modalProps}
          ref={ref}
          style={{
            background: 'white',
            color: 'black',
            padding: 30,
            width: '50%'
          }}>
          <h3
            {...titleProps}
            style={{ marginTop: 0 }}>
            {title}
          </h3>
          {children}
        </div>
      </FocusScope>
    </div>
  );
}

export default function Modal(props: AriaButtonProps<ElementType> | any) {
  let state = useOverlayTriggerState({});
  let ref: RefObject<any> = React.useRef();
  let { buttonProps } = useButton(props, ref);


  return <>
    <OverlayProvider>
      <Button onPress={state.open}>
        Open Dialog
      </Button>
      {state.isOpen && (
        <OverlayContainer>
          <ModalDialog
            title={props.title}
            isOpen
            onClose={state.close}
            isDismissable
          >
            <div
              style={{
                display: "flex",
                justifyContent: "center",
                alignItems: "center",
                flexDirection: "column",
              }}
            >
              {props.children}
            </div>
          </ModalDialog>
        </OverlayContainer>
      )}
    </OverlayProvider>
  </>;
}

In the above code snippet, we imported the Aria hooks we need for this component, we created a ModalDialog component to create a dialog for the modal. In the ModalDialog, we used the useOverly hook which returns the overlayProps and underlayProps props to handle the user interactivity outside a dialog and to close the modal. Then we used the useDialog hook which returns dialogProps and titleProps to get the props of the dialogue and its title. Also, we used the FocusScope component to specify the focus area to be controlled by the dialog.

Next, we created the Modal component, here we create the actual modal content. Here we used the useButton hook to ensure that focus management is handled correctly, across all browsers. Focus is restored to the button once the dialog closes. Lastly, we used the ModalDialog we created to create a dialog for the modal and pass in the required props. Also, we wrapped the application in an OverlayProvider hook so that it can be hidden from screen readers when a modal opens. You can learn more about creating a modal from this link.

Using React Aria Components

Now let's use the components libraries to create a small application. To do that, we'll create a Layout.tsx file in the components folder and add the code snippets below.

import { useMenu, useNavigation, LayoutProps } from "@pankod/refine-core";
import routerProvider from "@pankod/refine-react-router-v6";
import Button from "./Button";
import Input from "./Input";
import Header from "./Header";
import Modal from "./Modal";


export const Layout: React.FC<LayoutProps> = ({ children }) => {

    return (
        <div className="App">
            <Header className="primary">
                <p>Formak</p>
                <p>Signup</p>
            </Header>
            <Modal title="Signup Form">
                <form>
                    <Input label="Name" />
                    <Input label="Email" />
                    <Input label="Password" type="password" />
                    <Button style={{ backgroundColor: "red" }} onPress={() => console.log('button clicked')}>Signup</Button>
                </form>
            </Modal>
        </div>

    );
};

In the above code snippet, we imported the component libraries and used them to create a small form application. Now update the App.tsx file with the code snippet below.

import { Refine } from "@pankod/refine-core";
import routerProvider from "@pankod/refine-react-router-v6";
import dataProvider from "@pankod/refine-simple-rest";
import { Layout } from "components/Layout";

function App() {
  return (
    <Refine
      routerProvider={routerProvider}
      dataProvider={dataProvider}
      resources={[{name:"login"}]}
      Layout={Layout}
    />
  );
}

export default App;
import { Refine } from "@pankod/refine-core";
import routerProvider from "@pankod/refine-react-router-v6";
import dataProvider from "@pankod/refine-simple-rest";
import { Layout } from "components/Layout";

function App() {
  return (
    <Refine
      routerProvider={routerProvider}
      dataProvider={dataProvider}
      resources={[{name:"dummy"}]}
      Dashboard={Layout}
    />
  );
}

export default App;

You should see the output below on the browser. Using React Aria component

Now if you click the Open Dialog you should see the form modal below.

Opening React Aria dialog

You can use the tools like Rollup.tsx to bundle the component library and share it with your friends.

Adding Server side rendering

SSR, or server-side rendering, is the process of rendering components to HTML on the server as opposed to only on the client. A comparable strategy is static rendering, except instead of pre-rendering pages to HTML on each request but rather at build time. To make components using React Aria work with SSR, 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. Update the index.tsx file with the code snippet below.

...
import {SSRProvider} from '@react-aria/ssr';
...

...
ReactDOM.render(
  <SSRProvider>
  <React.StrictMode>
    <App />
  </React.StrictMode>
  </SSRProvider>,
  document.getElementById("root")
);
...

Conclusion

Throughout this tutorial, we’ve implemented how to create a component library in React using React Aria. We started by understanding what React Aria is and why you should consider using it for creating component libraries. Then we created some component libraries using React Aria and used it to build a signup form. You can learn more about React Aria from the official docs.