The Perfect Trio: Unlocking the Potential of React, Tailwind, and Radix-UI

sabin shrestha
readytowork, Inc.
Published in
9 min readAug 17, 2023
Article main image

Introduction:

Are you tired of writing the same CSS over and over again? Do you find yourself struggling to manage your React components? Look no further than the perfect trio: React, Tailwind, and Radix-UI.

You may be thinking, “But we already have React component libraries like MUI, Chakra, and antd that provide beautifully styled components for every need.” Well, you are not wrong however, while these libraries offer a wide range of components, they often come with a trade-off: their heavyweight nature and the need to override CSS extensively, leading to cumbersome and time-consuming customization, not to mention the frequent use of “!important” in your CSS to override default styling, which is never an ideal practice. When working on small to medium-scale projects, it’s often more efficient to seek out a lightweight alternative.

On the flip side, we have a lightweight alternative that seamlessly integrates with Tailwind CSS and provides a delightful development experience- Radix-UI.

What is a Radix-UI?

Radix-UI is simple, easy to use, and compatible with Tailwind CSS. Radix-UI offers Headless UI components with self-explaining names, making it easy for developers to understand their purpose and use them effortlessly. The lightweight nature of Radix-UI eliminates the need for overriding extensive CSS, enabling a smoother development workflow.

What is Tailwind CSS?

Tailwind CSS is a utility-first CSS framework that allows developers to style with pre-defined utility classes. This saves a lot of time since you don’t have to come up with a suitable class name for an element. Tailwind empowers developers to create visually stunning UIs without the need to write custom CSS and speeds up your development process as a front-end developer.

Now, Assuming you have a basic understanding of React, let’s dive straight into the magic that happens when React teams up with Tailwind CSS and Radix-UI with some demos.

Practical Implementation of React, Radix, and Tailwind:

To showcase the practical implementation of these three powerful tools, we will build two UI components: an accordion component and a toolbar component using Radix-UI and Tailwind CSS. I won’t be providing a detailed explanation of the code here, I assume you are already familiar with React. But, you’ll get a clear idea of the steps involved.

So, get ready to follow along as we build the accordion and toolbar components using Radix-UI and Tailwind CSS. The provided examples and visuals will give you a solid foundation to implement these components in your own projects.

First, let's get started with the React project setup with necessary dependencies.

Project Setup:

I personally prefer setting up my React application in Vite and that's what i’m doing here, you can also use the trusty old create-react-app to set up your React project.

yarn create vite

Now that we have our React app set up, we need to install Tailwind CSS and configure it. We can do this by running the following commands:

yarn add -D tailwindcss postcss autoprefixer

Next, execute this command in your project directory:

npx tailwindcss init -p

This command generates tailwind.config.js andpostcss.config.js configuration files, also known as config files. They help you interact with your project and customize everything.

Your tailwind.config.js the file will look something like this:

/** @type {import('tailwindcss').Config} */
export default {
content: [],
theme: {
extend: {},
},
plugins: [],
}

Add this piece of code to your content in tailwind.config.js file so it looks like this:

/** @type {import('tailwindcss').Config} */
export default {
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
theme: {
extend: {},
},
plugins: [],
};

Now finally copy and paste this code in the index.css file:

@tailwind base;
@tailwind components;
@tailwind utilities;

Let’s incorporate Radix-UI into our project setup as well. One of the advantages of Radix is that you don’t need to install all available components. Unlike libraries such as MUI or antd, where you have hundreds of components included by default, Radix allows you to selectively choose and include only the components you actually need. This provides a more efficient and streamlined development experience.

We only need Toolbar and accordion component so let's install them:

yarn add @radix-ui/react-toolbar @radix-ui/react-accordion 
yarn add @radix-ui/react-separator @radix-ui/react-icons

The above command installs Toolbar , Accordion , radix-react-icon that Radix provides. We are using radix-icon for icons. As for styling we are using Tailwind.

Let's start with a simple one: Accordion component.

// Let's create our separate Accordion trigger component

/**
* Start by importing and using the Radix-UI `Accordion`
* component to create a toolbar that is accessible and easy to use.
*/

// Radix UI
import * as Accordion from "@radix-ui/react-accordion";
import { ChevronDownIcon } from "@radix-ui/react-icons";

// Trigger component for accordion
const AccordionTrigger = React.forwardRef<
HTMLButtonElement,
{
children: React.ReactNode;
className?: string;
}
>(({ children, className, ...props }, forwardedRef) => (
<Accordion.Header>
<Accordion.Trigger
className={`
group
outline-none
px-5 h-[45px] w-full
flex items-center
justify-between
bg-slate-100
text-indigo-500
${className}`}
outline-none
{...props}
ref={forwardedRef}
>
{children}
<ChevronDownIcon
className="
rotate-0
duration-300
transition-transform
group-data-[state=closed]:rotate-180
ease-[cubic-bezier(0.87, 0, 0.13, 1)]
"
aria-hidden
/>
</Accordion.Trigger>
</Accordion.Header>
));

So, the accordion triggers are created using the AccordionTrigger component, which is a custom component defined using the React.forwardRef function. It renders a button with a down arrow icon, indicating the open or closed state of the accordion item. The styling is done using CSS classes and the Tailwind CSS utility classes.

Now let's create an accordion content component right down the line.

//...

// Accordion Content component

const AccordionContent = React.forwardRef<
HTMLDivElement,
{
className?: string;
children: React.ReactNode;
}
>(({ children, className, ...props }, forwardedRef) => (
<Accordion.Content
className={`
AccordionContent
bg-indigo-100
text-slate-700
overflow-hidden

data-[state=open]:animate-slide-down
data-[state=closed]:animate-slide-up

${className} `}
{...props}
ref={forwardedRef}
>
<div className="AccordionContentText px-5 py-[15px]">{children}</div>
</Accordion.Content>
));

Likewise, the accordion content is rendered using the AccordionContent component, also defined using the React.forwardRef function. It displays the content of each accordion item and includes CSS classes for styling.

Accordion uses data attributes to determine the state of the content hence, I’ve used the data-[state=open] and data-[state=closed] attributes to open and close accordion To get that slick animation/transition when content is hidden and displayed, you’ll need to add these two animation i.e. slide-down and slide-up in theme configuration within your tailwind.config.js file like this:

/** @type {import('tailwindcss').Config} */
export default {
content: ["./index.html", "./src/**/*.{js,jsx,ts,tsx}"],
theme: {
extend: {
boxShadow: {
outline: "0 0 0 2px rgba(0, 0, 0, 0.9)",
},
keyframes: {
slideDown: {
"0%": { height: "0" },
"100%": { height: `var(--radix-accordion-content-height)` },
},
slideUp: {
"0%": {
height: `var(--radix-accordion-content-height)`,
},
"100%": {
height: "0",
},
},
},
animation: {
"slide-down": "slideDown 300ms cubic-bezier(0.87, 0, 0.13, 1)",
"slide-up": "slideUp 300ms cubic-bezier(0.87, 0, 0.13, 1)",
},
},
},
plugins: [],
};

In the implementation, you may have observed that I utilized the CSS variable --radix-accordion-content-height to control the height of the content when it opens or closes. It's truly remarkable that Tailwind offers this dynamic height feature as a CSS variable named — radix-accordion-content-height. This functionality proves to be immensely useful, as it allows for greater flexibility and adaptability within the accordion component.

And now for the fun part let's create our Accordion component:

//...
import * as Separator from "@radix-ui/react-separator";

// constants
const commonAccItemStyling = `
overflow-hidden
focus-within:z-[1]
focus-within:relative
focus-within:shadow-outline
`;
const AccordionDemo = () => {
return (
<div className="flex justify-center py-5">
<Accordion.Root
className="w-[400px] rounded-md bg-slate-100 shadow-md"
type="single"
defaultValue="item-1"
collapsible
>
<Accordion.Item
className={`
rounded-tl-md
rounded-tr-md
${commonAccItemStyling}
`}
value="item-1"
>
<AccordionTrigger>Is it accessible?</AccordionTrigger>
<AccordionContent>
Yes. It adheres to the WAI-ARIA design pattern.
</AccordionContent>
</Accordion.Item>
<Separator.Root
color="#888"
className="SeparatorRoot bg-slate-300 h-[1px] w-full"
/>
<Accordion.Item className={`${commonAccItemStyling}`} value="item-2">
<AccordionTrigger className="">Is it unstyled?</AccordionTrigger>
<AccordionContent>
Yes. It's unstyled by default, giving you freedom over the look and
feel.
</AccordionContent>
</Accordion.Item>
<Separator.Root
color="#888"
className="SeparatorRoot bg-slate-300 h-[1px] w-full"
/>
<Accordion.Item
className={`
rounded-bl-md
rounded-br-md
${commonAccItemStyling}
`}
value="item-3"
>
<AccordionTrigger>Can it be animated?</AccordionTrigger>
<AccordionContent>
Yes. It's unstyled by default, giving you freedom over the look and
feel.
</AccordionContent>
</Accordion.Item>
</Accordion.Root>
</div>
);
};

Finally, the AccordionDemo The component renders a fully functional accordion with multiple items. It uses the Accordion.Root component as the container for the accordion. Each item in the accordion is represented by the Accordion.Item component, which includes an accordion trigger and content. The accordion can be expanded or collapsed by clicking on the triggers.

Accordion gif

There you go, you have created a beautiful-looking accordion that is fully accessible, supports keyboard navigation, and supports horizontal/vertical orientation with very few lines of code.

Next, let's create our Toolbar component.

To begin, we import the necessary components and icons from the Radix-UI library. These include the Toolbar component, as well as icons such as FontBoldIcon, FontItalicIcon, StrikethroughIcon, TextAlignLeftIcon, TextAlignRightIcon, and TextAlignCenterIcon.

Next, we define some constant styling variables that hold CSS classes and styles used throughout the toolbar. These variables help maintain consistent styling and enhance code reusability.

The main component ToolbarDemodemonstrates the usage of the toolbar. It renders the toolbar using the Toolbar.Root component as the container. The container has various CSS classes applied to it, defining its appearance and layout.

/**
* Import Toolbar Radix-UI component, and some required icons also.
*/

// package
import * as Toolbar from "@radix-ui/react-toolbar";
import {
FontBoldIcon,
FontItalicIcon,
StrikethroughIcon,
TextAlignLeftIcon,
TextAlignRightIcon,
TextAlignCenterIcon,
} from "@radix-ui/react-icons";
// constants
const toggleItemContainerStyling = `
flex items-center gap-[4px]
`;
const commonStyling = `
inline-flex
text-[14px]
outline-none
items-center
rounded-[4px]
text-slate-700
justify-center
flex-[0_0_auto]
ToolbarToggleItem
h-[25px] px-[4px]
hover:bg-indigo-100
hover:text-indigo-700
focus-within:ring-2
focus-within:relative
focus-within:ring-indigo-500
data-[state=on]:bg-indigo-200
data-[state=on]:text-indigo-700
`;
// In the accordion component, we use the data attribute 'data-[state=on]'
// to indicate whether a toggle item is selected or not. Based on this attribute,
// we apply specific styling to highlight the selected toggle item.
const separatorStyling = `
w-[1px] h-5
bg-slate-300
mx-2
`;
const ToolbarDemo = () => {
return (
<Toolbar.Root
className="ToolbarRoot
px-5 py-2 m-5
flex rounded-md
items-center
bg-slate-100 shadow-md
"
aria-label="Formatting options"
>
{/*
* Toolbar.ToggleGroup to group toggle items for text
* formatting and text alignment. Each toggle group contains
* individual Toolbar.ToggleItem components representing specific actions.
* These items are accompanied by relevant icons and labels for easy
* identification.
*/
}
<Toolbar.ToggleGroup
type="multiple"
className={`${toggleItemContainerStyling}`}
aria-label="Text formatting"
onValueChange={(value) => console.log(value)}
>
<Toolbar.ToggleItem
className={`${commonStyling}`}
value="bold"
aria-label="Bold"
>
<FontBoldIcon height={20} width={20} />
</Toolbar.ToggleItem>
<Toolbar.ToggleItem
className={`${commonStyling}`}
value="italic"
aria-label="Italic"
>
<FontItalicIcon height={20} width={20} />
</Toolbar.ToggleItem>
<Toolbar.ToggleItem
className={`${commonStyling}`}
value="strikethrough"
aria-label="Strike through"
>
<StrikethroughIcon height={20} width={20} />
</Toolbar.ToggleItem>
</Toolbar.ToggleGroup>
{/**
* Separators are added between groups or items using the
* Toolbar.Separator component
*/}
<Toolbar.Separator className={`${separatorStyling}`} />
<Toolbar.ToggleGroup
type="single"
defaultValue="center"
aria-label="Text alignment"
className={`${toggleItemContainerStyling}`}
>
<Toolbar.ToggleItem
className={`${commonStyling}`}
value="left"
aria-label="Left aligned"
>
<TextAlignLeftIcon height={20} width={20} />
</Toolbar.ToggleItem>
<Toolbar.ToggleItem
className={`${commonStyling}`}
value="center"
aria-label="Center aligned"
>
<TextAlignCenterIcon height={20} width={20} />
</Toolbar.ToggleItem>
<Toolbar.ToggleItem
className={`${commonStyling}`}
value="right"
aria-label="Right aligned"
>
<TextAlignRightIcon height={20} width={20} />
</Toolbar.ToggleItem>
</Toolbar.ToggleGroup>
<Toolbar.Separator className={`${separatorStyling}`} />
{
/**
* Toolbar.Link component to handle links within the toolbar
*/
}
<Toolbar.Link
className={`
${commonStyling}
bg-transparent

hover:bg-transparent
hover-text-indigo-700
focus-within:bg-transparent
focus-within:text-indigo-700
`}
href="#"
target="_blank"
style={{ marginRight: 10 }}
>
Edited 2 hours ago
</Toolbar.Link>
{/**
* A Toolbar.Button component to represent a button element.
*/
}
<Toolbar.Button
className={`
outline-none
rounded-[4px]
bg-indigo-600
text-slate-100
px-[14px] py-[5px] ml-auto
hover:bg-indigo-500
hover:text-slate-200
focus-within:ring-2
focus-within:ring-offset-2
focus-within:ring-indigo-500
`}
>
Share
</Toolbar.Button>
</Toolbar.Root>
);
};
export default ToolbarDemo;

Each component is customized with specific CSS classes and attributes to achieve the desired styling and functionality. I have also added an event handler that console logs selected values, but in your real project you want to interact with the value and use them as per your need.

After putting in and mixing our two main ingredients: Radix-UI and Tailwind we get this:

Radix-UI Toolbar

Congratulations, you have created a toolbar that completely supports keyboard navigation just by using components provided by Radix-UI.

Conclusion:

In conclusion, the combination of React, Tailwind CSS, and Radix-UI offers a powerful toolkit for building beautiful and customizable user interfaces. Beyond the examples discussed, Radix-UI offers a wide range of props, data attributes, and CSS variables that enable the creation of even more dynamic and complex components.

By leveraging these technologies, developers can create efficient workflows, save time on styling, and build accessible components. With Radix-UI’s flexibility and compatibility with Tailwind CSS, developers can create stunning UIs with minimal code. This perfect trio unlocks the potential for creating exceptional user interfaces while maintaining a streamlined development process.

--

--