When I joined EasyPost, it was evident that the front-end faced significant fragmentation due to multiple independent iterations by the design team. The user interfaces lacked consistency, developers contended with conflicting implementations, and outdated patterns created ongoing challenges.
I was tasked with leading the effort to resolve this fragmentation and unify our web platform. The solution needed to be flexible enough to meet the Design team's requirements while avoiding the strain of creating components from scratch.
One of our first decisions was whether to build a custom component library or adopt an existing one. While we explored popular options like Material UI and Chakra UI, the Design team's requirements demanded more flexibility than these libraries could provide.
At the same time, we wanted to avoid the complexities of managing advanced accessibility and cross-browser quirks ourselves, given the robust solutions already available in the community.
To strike a balance, we explored headless libraries that offered flexibility without sacrificing accessibility. After evaluating options like Radix UI and Headless UI, we chose React Aria. Its extensive component hooks and the maturity of development from Adobe made it the best fit for our design system.
Adopting React Aria hooks was a smooth process, thanks to Adobe's thoughtful design. The hooks encapsulate complex behaviors into simple, easy-to-use utilities.
Implementation typically involved passing props into a hook and spreading the returned props onto a DOM element, with React Aria seamlessly handling the functionality.
With its comprehensive set of hooks for various components and behaviors, React Aria made it easier than ever to create a component library with a fully custom look and feel.
Partway through our work on Easy UI, React Aria launched React Aria Components, building on their hooks and simplifying the process of creating a new component library even further.
export function Toggle(props: ToggleProps) {
const ref = React.useRef(null);
const state = useToggleState(props);
const { inputProps: inputPropsFromSwitch } = useSwitch(props, state, ref);
const { isFocusVisible, focusProps } = useFocusRing();
const { isHovered, hoverProps } = useHover(props);
const className = classNames(
styles.Toggle,
isFocusVisible && styles.focused,
isHovered && styles.hovere,
);
return (
<input
className={className}
ref={ref}
{...mergeProps(restProps, inputPropsFromSwitch, focusProps, hoverProps)}
/>
);
};
For styling, we chose CSS Modules. While not the newest solution, it proved to be highly effective for our needs.
The decision was driven by the team’s familiarity with CSS. Although we considered Tailwind, it felt too nascent for our team at the time. CSS Modules offered a familiar approach while providing the benefits of component-level encapsulation.
This choice has worked well for Easy UI, offering protection against a collision-prone cascade while maintaining a productive and straightforward authoring experience.
@use "../styles/common" as *;
.Toggle {
display: inline-flex;
align-items: center;
gap: design-token("space.1.5");
}
.text,
.input {
display: inline-flex;
}
import styles from "./Toggle.module.scss";
export function Toggle(props: ToggleProps) {
return (
<div className={styles.Toggle}>
<input className={styles.input} />
<span className={styles.text}>
{children}
</span>
</div>
);
};