This article was last updated on November 6, 2024, to include performance optimization tips and accessibility-focused animation handling techniques for improved user experiences in Radix UI when paired with Framer Motion.
Introduction
Radix
is an open-source headless library that provides components for creating user-friendly, high-quality React
online applications and design systems. It offers a wide range of accessible headless primitives, meant to expedite development by providing frequently used UI elements such as dialogues, selections, accordions, tabs, and more. In this article, we will illustrate how to build components with Radix.
Step we'll take in this article:
- Radix Building Blocks
- Adding Animations to Radix Components with Framer Motion
- Accessibility Best Practices in Radix UI
Radix Building Blocks
Radix consists of four building blocks: Primitives, Colors, Icons and Themes. These tools can be used to create comprehensive design systems. Let's take a deeper look at each product and see how they can be utilized independently or together to meet your needs.
Primitives
Radix
Primitives is a UI component library that helps you create high-quality, user-friendly design systems and web applications. It is a collection of low-level UI component libraries with a focus on accessibility, customization, and the developer experience. These components can be used as the foundation of your design system or added to it incrementally.
Radix
Primitives was designed with the goal of creating controllable headless components. All of its wiring is done internally, allowing you to start using the components as soon as possible.
The Components are also shipped with zero styles, providing you complete styling control. Components can be styled using any styling method (vanilla CSS
, CSS
preprocessors/frameworks, CSS-in-JS
libraries).
Here is an example of a Radix
accordion primitive
import * as Accordion from "@radix-ui/react-accordion";
export default () => (
<Accordion.Root type="single" defaultValue="item-1" collapsible>
<Accordion.Item value="item-1">
<Accordion.Header>
<Accordion.Trigger>This is an accordion</Accordion.Trigger>
</Accordion.Header>
<Accordion.Content>
Yes. It's unstyled by default, giving you freedom over the look and
feel.
</Accordion.Content>
</Accordion.Item>
</Accordion.Root>
);
The primitive also have access to props
which are listed here.
Radix provides a large number of primitives, including accordions, dialog boxes, popovers, dropdowns, labels, and many more. To check out those, visit here.
To view the list of Radix Primitives, visit here.
Colors
Radix Colors is a meticulously created color system used to create aesthethically pleasing web applications. Radix Colors includes a set of scales that are JavaScript objects designed to work with CSS to CSS-in-JS
alternatives (for example, styled-components
). It also bundles the colors as raw CSS
files which you can import straight into your files when using a bundler like Parcel or Webpack.
To view more on Radix Colors, visit here.
Icons
Radix Icons provides a set of 15×15
icons that are accessible as separate components and can be installed as a single package which you can import in your React components. Radix Icons are also accessible in other formats, including downloadable SVGs
, Figma, and Sketch files.
To view more on Radix Icons, visit here.
Themes
Radix Themes is a pre-styled component library that works out of the box and requires minimum configuration.
Radix Themes doesn't include a styling system. There are no CSS
or SX
props, and no styling libraries are used inside. It's made with standard CSS
. However, the themes can be customized by altering the token system's CSS
variables. The list of the variables supported by the token system is listed here.
This article will include explanations for the Reset, Grid, and Theme components. However, to see more components that themes provide, visit here.
Reset component
TheReset
component is used to forcibly reset browser styles for a specified element.
Under the hood, it generates a Slot
primitive (a Radix primitive) which does the following:
- Accepts one
React
element as its child. - Removes opinionated browser styles.
- Sets idiomatic layout defaults, such as
display: block
for photos orwidth: Stretch
for inputs. - Sets the cursor style based on your theme settings.
- Adds the property
box-sizing: border-box
.
An example of the Reset
component in use is illustrated in the code below:
import { Reset } from "@radix-ui/themes";
function App() {
return (
<div>
<div className="flex gap-2">
<div className="w-6/12">
<h1>Without Reset Component</h1>
<abbr title="Abbreviation">ABR</abbr>
</div>
<div className="w-6/12">
<h1>With Reset Component</h1>
<Reset>
<abbr title="Abbreviation">ABR</abbr>
</Reset>
</div>
</div>
</div>
);
}
View:
Grid component
The Grid
component is a component for designing grid layouts. This component is based on the div
element and accepts common margin props.
An example of the Grid
component in use is illustrated in the code below:
import { Grid, Box } from "@radix-ui/themes";
function App() {
return (
<div>
<Grid columns="3" gap="3" rows="repeat(3, 40px)" width="auto">
<Box
width={"200px"}
height={"50px"}
className="bg-black text-center text-white"
>
Box 1
</Box>
<Box
width={"200px"}
height={"50px"}
className="bg-black text-center text-white"
>
Box 2
</Box>
<Box
width={"200px"}
height={"50px"}
className="bg-black text-center text-white"
>
Box 3
</Box>
<Box
width={"200px"}
height={"50px"}
className="bg-black text-center text-white"
>
Box 4
</Box>
<Box
width={"200px"}
height={"50px"}
className="bg-black text-center text-white"
>
Box 5
</Box>
<Box
width={"200px"}
height={"50px"}
className="bg-black text-center text-white"
>
Box 6
</Box>
</Grid>
</div>
);
}
View:
Theme component
The Theme
component wraps all or portion of a React tree to enable theme customization.
An example of the Theme
component in use is illustrated in the code below:
import {
Grid,
Button,
TextArea,
Heading,
Flex,
Card,
Theme,
Text,
} from "@radix-ui/themes";
function App() {
return (
<div>
<Card size="2">
<Flex gap="6">
<Flex direction="column" gap="3">
<Heading as="h5" size="2">
Regular card
</Heading>
<Grid gap="1">
<Text as="div" weight="bold" size="2" mb="1">
Feedback
</Text>
<TextArea placeholder="Write your feedback…" />
</Grid>
<Button>Send</Button>
</Flex>
<Theme accentColor="cyan" radius="full">
<Card size="2">
<Flex gap="6">
<Flex direction="column" gap="3">
<Heading as="h5" size="2">
Card with cyan theme
</Heading>
<Grid gap="1">
<Text as="div" weight="bold" size="2" mb="1">
Feedback
</Text>
<TextArea placeholder="Write your feedback…" />
</Grid>
<Button>Send</Button>
</Flex>
<Theme accentColor="orange">
<Card size="2">
<Flex direction="column" gap="3">
<Heading as="h5" size="2">
Card with orange theme
</Heading>
<Grid gap="1">
<Text as="div" weight="bold" size="2" mb="1">
Feedback
</Text>
<TextArea placeholder="Write your feedback…" />
</Grid>
<Button>Send</Button>
</Flex>
</Card>
</Theme>
</Flex>
</Card>
</Theme>
</Flex>
</Card>
</div>
);
}
According to the code, cards wrapped in the Cyan and Orange themes reflected these themes on their cards, whilst cards that were not wrapped in the theme reflected Radix's
default theme.
View:
Setup Radix in your React Project
- First, we'll develop a React application. We can achieve this with Vite or CRA. However, Vite is highly recommended.
Installing Primitives
To install a primitive component, add the name of the component to a
radix-ui/
prefix as shown in the example below:# with npm
npm install @radix-ui/react-popover@latest -E
# with yarn
yarn add @radix-ui/react-popover@latest -Eimport the parts of the primitive as shown below:
import * as React from "react";
import * as Popover from "@radix-ui/react-popover";
const PopoverDemo = () => (
<Popover.Root>
<Popover.Trigger>More info</Popover.Trigger>
<Popover.Portal>
<Popover.Content>
Some more info…
<Popover.Arrow />
</Popover.Content>
</Popover.Portal>
</Popover.Root>
);
export default PopoverDemo;Add styles to the primitives
javascript
:import * as React from "react";
import * as Popover from "@radix-ui/react-popover";
import "./styles.css";
const PopoverDemo = () => (
<Popover.Root>
<Popover.Trigger className="PopoverTrigger">Show info</Popover.Trigger>
<Popover.Portal>
<Popover.Content className="PopoverContent">
Some content
<Popover.Arrow className="PopoverArrow" />
</Popover.Content>
</Popover.Portal>
</Popover.Root>
);
export default PopoverDemo;CSS:
/* styles.css */
.PopoverTrigger {
background-color: white;
border-radius: 4px;
}
.PopoverContent {
border-radius: 4px;
padding: 20px;
width: 260px;
background-color: white;
}
.PopoverArrow {
fill: white;
}
Installing Themes
To install Themes, type the command below on your terminal:
# with npm
npm install @radix-ui/themes
# with yarn
yarn add @radix-ui/themesNext, import the styles to the entrypoint of your application
import '@radix-ui/themes/styles.css';
Add the Theme import to the entrypoint of the application()
import "@radix-ui/themes/styles.css";
import { Theme } from "@radix-ui/themes";
export default function () {
return (
<Theme>
<MyApp />
</Theme>
);
}Start adding the themes components to your application
import { Flex, Text, Button } from "@radix-ui/themes";
export default function MyApp() {
return (
<Flex direction="column" gap="2">
<Text>Hello from Radix Themes :)</Text>
<Button>Let's go</Button>
</Flex>
);
}
Installing Icons
To install colors type the command below:
# with npm
npm install @radix-ui/react-icons
# with yarn
yarn add @radix-ui/react-iconsImport the Icons into your project
import { FaceIcon, ImageIcon, SunIcon } from "@radix-ui/react-icons";
function MyComponent() {
return (
<div>
<FaceIcon />
<SunIcon />
<ImageIcon />
</div>
);
}
Installing Colors
To install colors, type the command below on your terminal
# with npm
npm install @radix-ui/colors
# with yarn
yarn add @radix-ui/colors
Vanilla CSS
Radix Colors gives the colors as raw CSS
files. When using a bundler like Parcel or Webpack, you can simply import them into your files.
/* Import only the scales you need */
@import '@radix-ui/colors/gray.css';
@import '@radix-ui/colors/blue.css';
@import '@radix-ui/colors/green.css';
@import '@radix-ui/colors/red.css';
@import '@radix-ui/colors/gray-dark.css';
@import '@radix-ui/colors/blue-dark.css';
@import '@radix-ui/colors/green-dark.css';
@import '@radix-ui/colors/red-dark.css';
/* Use the colors as CSS variables */
.button {
background-color: var(--blue-4);
color: var(--blue-11);
border-color: var(--blue-7);
}
.button:hover {
background-color: var(--blue-5);
border-color: var(--blue-8);
}
Component Objects
Radix Colors can also be provided as Objects
that can be incorporated into any React
project.
import {
gray,
blue,
red,
green,
grayDark,
blueDark,
redDark,
greenDark,
} from "@radix-ui/colors";
Adding Animations to Radix Components with Framer Motion
If you want to give interactions with the components of Radix UI a more fluent look by adding an animation, combining Framer Motion with Radix can be quite powerful. Framer Motion interacts really seamlessly with the headless components of Radix while giving control over animating states, keeping things accessible.
Why Use Framer Motion with Radix?
Radix UI consists of completely headless components, meaning there are no animations. The power of Framer Motion is that it lets us take these otherwise static Radix elements, like modals, dropdowns, and tooltips, and animate them in a really declarative and reusable manner. This will make the interactions feel way more smooth and engaging, but since Framer Motion works in a declarative way, that means we can keep our animations consistent and accessible.
Install Framer Motion:
Then, install Framer Motion if it’s not already in the project:
npm install framer-motion
The wrapping of radix components with animation controls:
Wrap radix elements in Framer Motion’s components, such as motion.div, which can handle animations related to entrance, exit, and interaction states. Define Animation Variants:
Framer Motion will allow us to define variants of animations—hidden/visible states we can toggle depending on the state of the component. So, opening a modal would slap a visible variant on it and fade or slide it into view, for example.
The example below represents how you might animate a Radix dialog using Framer Motion. This setup will apply a fade in when the dialog opens, and a fade out on close:
import * as Dialog from "@radix-ui/react-dialog";
import { motion } from "framer-motion";
const modalVariants = {
hidden: { opacity: 0, y: -20 },
visible: { opacity: 1, y: 0 },
};
function AnimatedDialog() {
return (
<Dialog.Root>
<Dialog.Trigger>Open Dialog</Dialog.Trigger>
<Dialog.Portal>
<Dialog.Overlay asChild>
<motion.div
initial="hidden"
animate="visible"
exit="hidden"
variants={modalVariants}
transition={{ duration: 0.3 }}
className="fixed inset-0 bg-black bg-opacity-50"
/>
</Dialog.Overlay>
<Dialog.Content asChild>
<motion.div
initial="hidden"
animate="visible"
exit="hidden"
variants={modalVariants}
transition={{ duration: 0.3 }}
className="fixed left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 rounded-md bg-white p-6"
>
<h2>Animated Radix Dialog</h2>
<p>This dialogue fades in and out nicely!</p>
<Dialog.Close>Close</Dialog.Close>
</motion.div>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
);
}
Tips for Smooth Animations • Exit Transitions: Make the exit transitions pop for dialog or overlay close smoothly. • Timing for Transition: Edit the transition times in order to harbor smooth motion, without anything going too slow or fast. • Accessibility Reduce Motion: Remember that for some users, they like having minimal motion. You should consider adding a prefers-reduced-motion media query in order to turn off animations when this is enabled.
Build components with Radix
This article will include sample components for Dialogs and Slider.
Dialogs
The anatomy of a Dialog with Radix consists of the following eight elements:
- Root: Contains all the components of a Dialog.
- Trigger: Contains the button that will be used to trigger the Dialog.
- Portal: When used, it move your overlay and content elements into the
body
element in the DOM. - Overlay: This is a layer that, when the dialog is open, covers the inactive area of the view.
- Content: Contains holds the contents of the dialog.
- Title: THolds the title of the content when the dialog is opened.
- Description: Holds the description of the content when the dialog is opened.
- Close: Holds the button that closes the dialog.
The code of a Dialog using Radix
primitives and Tailwind
classes is shown below:
import * as Dialog from "@radix-ui/react-dialog";
import { Cross2Icon } from "@radix-ui/react-icons";
function App() {
return (
<div style={{ marginTop: "150px" }}>
<Dialog.Root>
<Dialog.Trigger asChild>
<button className="text-violet11 shadow-blackA4 hover:bg-mauve3 inline-flex h-[35px] items-center justify-center rounded-[4px] bg-white px-[15px] font-medium leading-none shadow-[0_2px_10px] focus:shadow-[0_0_0_2px] focus:shadow-black focus:outline-none">
Edit profile
</button>
</Dialog.Trigger>
<Dialog.Portal>
<Dialog.Overlay className="bg-blackA6 data-[state=open]:animate-overlayShow fixed inset-0" />
<Dialog.Content className="data-[state=open]:animate-contentShow fixed left-[50%] top-[50%] max-h-[85vh] w-[90vw] max-w-[450px] translate-x-[-50%] translate-y-[-50%] rounded-[6px] bg-white p-[25px] shadow-[hsl(206_22%_7%_/_35%)_0px_10px_38px_-10px,_hsl(206_22%_7%_/_20%)_0px_10px_20px_-15px] focus:outline-none">
<Dialog.Title className="text-mauve12 m-0 text-[17px] font-medium">
Edit profile
</Dialog.Title>
<Dialog.Description className="text-mauve11 mb-5 mt-[10px] text-[15px] leading-normal">
Make changes to your profile here. Click save when you're done.
</Dialog.Description>
<fieldset className="mb-[15px] flex items-center gap-5">
<label
className="text-violet11 w-[90px] text-right text-[15px]"
htmlFor="name"
>
Name
</label>
<input
className="text-violet11 shadow-violet7 focus:shadow-violet8 inline-flex h-[35px] w-full flex-1 items-center justify-center rounded-[4px] px-[10px] text-[15px] leading-none shadow-[0_0_0_1px] outline-none focus:shadow-[0_0_0_2px]"
id="name"
defaultValue="Pedro Duarte"
/>
</fieldset>
<fieldset className="mb-[15px] flex items-center gap-5">
<label
className="text-violet11 w-[90px] text-right text-[15px]"
htmlFor="username"
>
Username
</label>
<input
className="text-violet11 shadow-violet7 focus:shadow-violet8 inline-flex h-[35px] w-full flex-1 items-center justify-center rounded-[4px] px-[10px] text-[15px] leading-none shadow-[0_0_0_1px] outline-none focus:shadow-[0_0_0_2px]"
id="username"
defaultValue="@peduarte"
/>
</fieldset>
<div className="mt-[25px] flex justify-end">
<Dialog.Close asChild>
<button className="bg-green4 text-green11 hover:bg-green5 focus:shadow-green7 inline-flex h-[35px] items-center justify-center rounded-[4px] px-[15px] font-medium leading-none focus:shadow-[0_0_0_2px] focus:outline-none">
Save changes
</button>
</Dialog.Close>
</div>
<Dialog.Close asChild>
<button
className="text-violet11 hover:bg-violet4 focus:shadow-violet7 absolute right-[10px] top-[10px] inline-flex h-[25px] w-[25px] appearance-none items-center justify-center rounded-full focus:shadow-[0_0_0_2px] focus:outline-none"
aria-label="Close"
>
<Cross2Icon />
</button>
</Dialog.Close>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
</div>
);
}
View:
Slider
The following four components make up the anatomy of a Slider made with Radix:
- Root: Comprising every component of a slider
- Track: The Slider-containing track.
- Range: The portion of the range that has to fit inside the slider.
- Thumb: The thumb can be moved.
The code of a Slider using Radix
primitives and Tailwind
classes is shown below:
import * as Slider from "@radix-ui/react-slider";
function App() {
return (
<div>
<Slider.Root
className="relative flex h-5 w-[200px] touch-none select-none items-center"
defaultValue={[50]}
max={100}
step={1}
>
<Slider.Track className="relative h-[3px] grow rounded-full bg-black">
<Slider.Range className="absolute h-full rounded-full bg-red-100" />
</Slider.Track>
<Slider.Thumb
className="shadow-blackA4 hover:bg-violet3 focus:shadow-blackA5 block h-5 w-5 rounded-[10px] bg-red-100 shadow-[0_2px_10px] focus:shadow-[0_0_0_5px] focus:outline-none"
aria-label="Volume"
/>
</Slider.Root>
</div>
);
}
View:
Accessibility Best Practices in Radix UI
I wanted to share some key points on accessibility when working with Radix UI. Radix UI is known for its focus on accessible components, but it’s always good to double-check and ensure our implementations fully support inclusive design. Here’s a quick rundown of accessibility considerations when building components with Radix UI.
Why Accessibility Matters in Radix UI
Radix UI provides a strong foundation with accessible primitives, ensuring that core elements like buttons, modals, and accordions are keyboard-friendly, screen-reader-compatible, and accessible by default. Even so, reviewing accessibility settings in complex components, especially when customizing, is essential.
Key Accessibility Considerations
Keyboard Navigation:
Radix UI components support keyboard navigation by default, making it easy to tab through interactive elements. For components like dropdowns or sliders, ensure they retain focus states and are easy to navigate with both keyboard and mouse.
Aria Attributes:
Radix UI uses appropriate ARIA roles and attributes by default, but we should verify them when we modify components. For example, if you add custom elements within a modal or dialog, confirm that ARIA roles like aria-labelledby
and aria-describedby
are accurately set to ensure screen readers can announce elements correctly.
Focus Management:
Radix UI provides automatic focus trapping for dialogs and modals. This feature keeps keyboard focus within a modal until it’s closed, which is great for user experience. However, if you add nested focusable elements, make sure they fit naturally into this flow.
Color Contrast and Theming:
When using Radix Colors, check color contrast ratios to ensure compliance with WCAG standards. While Radix colors are designed for accessibility, specific color customizations might need additional contrast adjustments for readability.
Customizable Announcements:
Some Radix components, like alerts or notifications, don’t automatically include screen reader announcements. If you’re building components that convey important status messages, consider adding aria-live
regions for real-time updates so screen readers alert users promptly.
Example Code for an Accessible Modal
Here’s a snippet to ensure focus is properly managed within a Radix dialog:
import * as Dialog from "@radix-ui/react-dialog";
function AccessibleModal() {
return (
<Dialog.Root>
<Dialog.Trigger>Open Modal</Dialog.Trigger>
<Dialog.Portal>
<Dialog.Overlay />
<Dialog.Content
aria-labelledby="dialog-title"
aria-describedby="dialog-description"
>
<h2 id="dialog-title">Welcome</h2>
<p id="dialog-description">This is an accessible modal example.</p>
<button onClick={() => Dialog.Root.close()}>Close</button>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
);
}
This setup uses ARIA attributes and manages focus effectively, helping make our modal both accessible and easy to navigate.
Integrating these practices should make a noticeable difference in user experience, especially for users relying on assistive technologies.
Conclusion
In this article, we looked at Radix
, discussed Radix's
Primitives, Colors, Icons, and Themes, and finally built components with Radix Primitives. Radix is an outstanding component library that you should consider using if you want to create design systems or visually appealing web apps.