Skip to content

Commit bffff9e

Browse files
committed
feat(article): React Aria migration
1 parent 70d8910 commit bffff9e

File tree

5 files changed

+238
-1
lines changed

5 files changed

+238
-1
lines changed

app/layout.tsx

-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import { Metadata } from "next";
44
import PlausibleProvider from "next-plausible";
55
import { Inter } from "next/font/google";
66
import localFont from "next/font/local";
7-
import { Suspense } from "react";
87

98
import { TooltipProvider } from "@/components/Tooltip";
109
import "@/styles/globals.css";
+238
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
1+
---
2+
title: "Migrating from Radix to React Aria: Improving Accessibility and UX"
3+
description: "We migrated from Radix and Ariakit to React Aria to enhance accessibility, improving keyboard navigation, screen reader support, and overall user experience."
4+
category: Tech
5+
author: Greg Bergé
6+
date: 2024-05-27
7+
image: ./main.jpg
8+
imageAlt: Push to reset the world.
9+
---
10+
11+
In the past seven years, Argos has utilized various UI libraries. We started with Material UI, then moved to Ariakit, Radix, and finally chose React Aria.
12+
13+
<MainImage
14+
credit={
15+
<>
16+
Photo by{" "}
17+
<a href="https://unsplash.com/@mpalmtree?utm_content=creditCopyText&utm_medium=referral&utm_source=unsplash">
18+
Manuel Palmeira
19+
</a>{" "}
20+
on{" "}
21+
<a href="https://unsplash.com/photos/a-black-and-white-photo-of-a-parking-meter-xO9Qxc5iRJs?utm_content=creditCopyText&utm_medium=referral&utm_source=unsplash">
22+
Unsplash
23+
</a>
24+
</>
25+
}
26+
/>
27+
28+
## Why React Aria?
29+
30+
### Accessibility and UX First
31+
32+
To me, [React Aria](https://react-spectrum.adobe.com/react-aria/) is the most advanced component library in terms of UX and accessibility. [Devon Govett](https://twitter.com/devongovett) and his team have done an incredible job creating low-level hooks and components that provide a top-tier experience.
33+
34+
The attention to detail is remarkable. The [press event of the button](https://react-spectrum.adobe.com/blog/building-a-button-part-1.html) is designed to work seamlessly across all platforms, fixing common issues with `:active` and `:hover`. In contrast, Radix doesn't even provide a button component.
35+
36+
Additionally, the work on [the submenu](https://react-spectrum.adobe.com/blog/creating-a-pointer-friendly-submenu-experience.html) shows their commitment to delivering an outstanding user experience.
37+
38+
### Backed by a Big Company
39+
40+
React Aria underpins React Spectrum, Adobe's design system used for web applications like Photoshop. Adobe's significant investment in this technology ensures long-term support and reliability. While [Ariakit](https://ariakit.org/) is excellent and [Diego Haz](https://x.com/diegohaz) is a talented developer, the lack of corporate backing poses sustainability risks. The project could halt if the developer decides to stop, and there's a higher risk of breaking changes due to the lack of a company-backed roadmap. [Radix](https://radix-ui.com/), supported by [WorkOS](https://workos.com/), also cannot match Adobe's resources and focus.
41+
42+
## Migration Strategy
43+
44+
Despite Argos's relatively small codebase, migrating our numerous UI components was challenging. We had two choices:
45+
46+
- Migrate component by component in several PRs
47+
- Migrate all at once in a single PR
48+
49+
I opted for the latter due to my deep project knowledge and confidence in our visual testing capabilities.
50+
51+
### Experience and Knowledge
52+
53+
With over eight years of React experience and extensive familiarity with Argos, I could thoroughly test and ensure everything worked correctly. Having built and maintained various UI libraries, including [Smooth UI](https://github.com/smooth-code/smooth-ui), I felt prepared for this comprehensive migration.
54+
55+
### Confidence with Visual Testing
56+
57+
Visual testing, a core feature of Argos, provided the confidence needed to ensure the UI remained consistent throughout the migration. Argos uses its own visual testing capabilities to capture UI snapshots and compare them against baseline images, allowing us to detect any unintended changes. This automated process ensured that even the smallest visual discrepancies were identified and addressed promptly. Migrating components individually would have been challenging due to React Aria’s tightly integrated system, but visual testing allowed us to confidently migrate everything at once, ensuring a smooth and accurate transition.
58+
59+
## Difficulties
60+
61+
React Aria is highly accessible, adhering strictly to ARIA patterns, which sometimes means certain practices are not allowed. While libraries like Ariakit or Radix offer flexibility to bypass some accessibility rules, React Aria does not compromise. This strict adherence ensures a genuinely accessible experience but comes with some limitations that require creative solutions.
62+
63+
### The Tooltip Problem
64+
65+
For instance, it's impossible to put a tooltip on something that is not focusable. Tooltips only work when the targeted component has a `useFocusable` hook. This was challenging because we have many tooltips on non-focusable elements. I created a `TooltipTarget` component to inject `focusableProps` and added `tabIndex: 0` to ensure the element is focusable.
66+
67+
**[`TooltipTarget` in Argos code](https://github.com/argos-ci/argos/blob/4822931b05c78e1b4a79e15cf4437fb0297369a6/apps/frontend/src/ui/Tooltip.tsx#L87)**
68+
69+
```tsx
70+
function TooltipTarget(props: { children: React.ReactElement }) {
71+
const triggerRef = React.useRef(null);
72+
const { focusableProps } = useFocusable(props.children.props, triggerRef);
73+
74+
return React.cloneElement(
75+
props.children,
76+
mergeProps(focusableProps, { tabIndex: 0 }, props.children.props, {
77+
ref: triggerRef,
78+
}),
79+
);
80+
}
81+
```
82+
83+
### Putting Tooltips on Disabled Buttons
84+
85+
While [Ariakit allows creating buttons that are accessible when disabled](https://ariakit.org/reference/button#accessiblewhendisabled), React Aria does not. They follow the spec strictly. [They suggest using contextual help](https://github.com/adobe/react-spectrum/issues/3662#issuecomment-1743658584) because tooltips are not fully accessible for sharing information. Although they are correct, sometimes it feels necessary to put a tooltip on a disabled button. For this, I wrapped my button in a `div`, even if it's not ideal.
86+
87+
**[Disabled button in Argos codebase](https://github.com/argos-ci/argos/blob/4822931b05c78e1b4a79e15cf4437fb0297369a6/apps/frontend/src/pages/Build/header/ReviewButton.tsx#L129-L137)**
88+
89+
```ts
90+
export function DisabledReviewButton(props: { tooltip: React.ReactNode }) {
91+
return (
92+
<Tooltip content={props.tooltip}>
93+
<div>
94+
<Button isDisabled>Review changes</Button>
95+
</div>
96+
</Tooltip>
97+
);
98+
}
99+
```
100+
101+
### Menus are Menus
102+
103+
Before the migration, Argos' user menu was created using Ariakit, including a theme selector. It was neat but impossible to replicate with React Aria. React Aria only allows specific components like `MenuItems`, `Section`, and `Header` in a menu. Attempting to use anything else throws an error and crashes.
104+
105+
![Menu with theme selector](/assets/articles/react-aria-migration/menu-with-theme-selector.jpg)
106+
107+
I embraced the menu structure by replacing the select with a submenu. This improved the experience by reducing clicks and enhancing item visibility.
108+
109+
![Menu with theme submenu](/assets/articles/react-aria-migration/menu-with-theme-submenu.jpg)
110+
111+
## The Good Parts
112+
113+
### Links
114+
115+
React Aria's link components are versatile, abstracting the router and working universally across the application. Absolute links use native anchors, while relative ones navigate using the provided `navigate` function. The `useHref` hook gives full `href` resolution, which is excellent for advanced routers like react-router that support nested links.
116+
117+
**[`RouterProvider` in Argos codebase](https://github.com/argos-ci/argos/blob/4822931b05c78e1b4a79e15cf4437fb0297369a6/apps/frontend/src/router.tsx#L1-L41)**
118+
119+
```tsx
120+
import { RouterProvider } from "react-aria-components";
121+
import {
122+
type NavigateOptions,
123+
Outlet,
124+
useHref,
125+
useNavigate,
126+
} from "react-router-dom";
127+
128+
declare module "react-aria-components" {
129+
interface RouterConfig {
130+
routerOptions: NavigateOptions;
131+
}
132+
}
133+
134+
function useAbsoluteHref(path: string) {
135+
const relative = useHref(path);
136+
if (
137+
path.startsWith("https://") ||
138+
path.startsWith("http://") ||
139+
path.startsWith("mailto:")
140+
) {
141+
return path;
142+
}
143+
return relative;
144+
}
145+
146+
function Root() {
147+
const navigate = useNavigate();
148+
149+
return (
150+
<RouterProvider navigate={navigate} useHref={useAbsoluteHref}>
151+
<Outlet />
152+
</RouterProvider>
153+
);
154+
}
155+
```
156+
157+
### Interactions (Hover, Pressed)
158+
159+
One issue I faced before React Aria was styling the `:hover` effect. `:hover` is applied even if the button is disabled, and you have to avoid this by using tricks like `[&:not([aria-disabled])]:hover]`.
160+
161+
React Aria emulates `:hover` and `:active`, replacing them with `[data-hovered]` and `[data-pressed]`. This fixes all issues: `[data-hovered]` is not present when the button is disabled. `[data-pressed]` fixes the issue where `:active` is applied even if you move your pointer outside the button. This behavior is correct because if you release your mouse button while not hovering over the button, it will not be clicked, so the style should not indicate it will be!
162+
163+
### Composition
164+
165+
I love the composition model used by React Aria Components. For example, a dialog is composed like this:
166+
167+
```tsx
168+
<DialogTrigger>
169+
<Button>Sign up…</Button>
170+
<Modal>
171+
<Dialog>
172+
{({ close }) => (
173+
<form>
174+
<Heading slot="title">Sign up</Heading>
175+
<TextField autoFocus>
176+
<Label>First Name</Label>
177+
<Input />
178+
</TextField>
179+
<TextField>
180+
<Label>Last Name</Label>
181+
<Input />
182+
</TextField>
183+
<Button onPress={close} style={{ marginTop: 8 }}>
184+
Submit
185+
</Button>
186+
</form>
187+
)}
188+
</Dialog>
189+
</Modal>
190+
</DialogTrigger>
191+
```
192+
193+
It's also possible to use the same dialog wrapped in a `Popover` to make it non-modal and contextual to one element.
194+
195+
Each element has its own responsibilities, making composition a breeze. For example, in Argos, I have [a `Popover` component](https://github.com/argos-ci/argos/blob/main/apps/frontend/src/ui/Popover.tsx) used for `Select` and `Menu`. It is responsible for the animation and the container style.
196+
197+
### Context
198+
199+
React Aria Components are designed with a clear and practical approach. For typical use cases, they are very straightforward, relying on a composition of components. However, if you need to implement more advanced functionality, you can access the internals using hooks and context. This dual approach offers both simplicity for common tasks and flexibility for more complex requirements.
200+
201+
For example, you can create a reusable `DialogDismiss` component by using `OverlayTriggerStateContext` to access the `close` function:
202+
203+
**[Example of `DialogDismiss` used in Argos codebase](https://github.com/argos-ci/argos/blob/4822931b05c78e1b4a79e15cf4437fb0297369a6/apps/frontend/src/ui/Dialog.tsx#L85-L112)**
204+
205+
```tsx
206+
const DialogDismiss = forwardRef<
207+
HTMLButtonElement,
208+
{
209+
children: React.ReactNode;
210+
onPress?: ButtonProps["onPress"];
211+
single?: boolean;
212+
}
213+
>((props, ref) => {
214+
const state = useContext(OverlayTriggerStateContext);
215+
return (
216+
<Button
217+
ref={ref}
218+
className={props.single ? "flex-1 justify-center" : undefined}
219+
variant="secondary"
220+
onPress={(event) => {
221+
props.onPress?.(event);
222+
state.close();
223+
}}
224+
autoFocus
225+
>
226+
{props.children}
227+
</Button>
228+
);
229+
});
230+
```
231+
232+
It makes really thing a breeze to compose with.
233+
234+
## Conclusion
235+
236+
React Aria stands out as the best UI library I've used. Its solid foundation, meticulous attention to detail, and unwavering commitment to accessibility make it a top choice for modern web applications. The library not only simplifies the implementation of accessible components but also ensures a seamless user experience across all platforms. Backed by Adobe, React Aria promises long-term support and reliability. This migration has significantly enhanced Argos, proving that prioritizing accessibility and user experience is not only beneficial but essential for creating outstanding web applications.
237+
238+
For more details, check out the [pull-request on GitHub](https://github.com/argos-ci/argos/pull/1302).
180 KB
Loading
Loading
Loading

0 commit comments

Comments
 (0)