Building a design system with Radix
Tags: webdev
Published: 2022-10-14 13:57:23

**Written by [Alyssa Holland](https://blog.logrocket.com/author/alyssaholland/)✏️**

[Radix](https://www.radix-ui.com) is an open source library that provides components for building accessible, high-quality React web applications and design systems. Radix is part of a new wave of headless components being created in the frontend space, and this pattern has been gaining a lot of traction lately. 

This guide aims to provide you with a deeper understanding of Radix and how it can be used to build out design systems and component libraries. The latter half of this article provides an in-depth tutorial on building a sample component library with Radix Primitives. 🎨

## Contents

*   [Radix overview](#radix-overview)
    *   [Primitives](#primitives)
    *   [Colors](#colors)
    *   [Icons](#icons)
*   [Design systems vs. component libraries](#design-systems-vs-component-libraries)
*   [Installing and setting up the sample project](#installing-and-setting-up-the-sample-project)
*   [Building a range input component with Radix](#building-a-range-input-component-with-radix)
    *   [Installation and boilerplate](#installation-and-boilerplate)
    *   [Styling the range input component](#styling-the-component)
    *   [Adding props](#adding-props)
*   [Building a tabs component with Radix](#building-a-tabs-component-with-radix)
    *   [Installation and boilerplate](#installation-and-boilerplate-1)
    *   [Adding props](#adding-props-1)
    *   [Styling the tabs component](#styling-the-component-1)

## Radix overview

Radix is composed of a suite of three products: Primitives, Colors, and Icons. These tools can be used to build out design systems. Let’s take a closer look at each individual product and how they can be used separately or collaboratively to suit your needs.

### Primitives

[Radix Primitives](https://www.radix-ui.com) is the library’s flagship product. It is described as a collection of “unstyled, accessible components for building high‑quality design systems and web apps in React.” Instead of reinventing the wheel, Radix Primitives handle a lot of the tricky parts when it comes to building out common component patterns. 

Building out accessible widgets that meet the [WAI-ARIA standards](https://www.w3.org/WAI/ARIA/apg/patterns/) and properly handle aspects like keyboard navigation can be a large undertaking. Libraries like Radix aim to simplify this often difficult process for developers by providing a set of flexible and extensible components that are prebuilt with accessibility and developer experience in mind.

### Colors

[Radix Colors](https://www.radix-ui.com/colors) is a carefully crafted color system designed for building stunning web apps. It provides additional features like automatic dark mode compatibility which makes switching to a dark theme as simple as applying a class to a container, the ability to [compose color palettes](https://www.radix-ui.com/docs/colors/palette-composition/composing-a-palette) to work with your app’s brand or theme, and accessibility considerations that are guaranteed to pass WCAG contrast ratio requirements. 

Radix Colors provides a set of scales that are JavaScript objects intended to integrate with your preferred styling solution, ranging from vanilla CSS to CSS-in-JS options (e.g., [styled-components](https://styled-components.com)).

### Icons

[Radix Icons](https://icons.radix-ui.com/) touts itself as a “_crisp set of 15x15 icons designed by the WorkOS team._” All the icons are available as individual components that can be installed via a single package. 

Simply import the respective icons, and you can add them to your apps like you would any other React component. Radix Icons are also available in other forms like downloadable SVGs, along with Figma and Sketch files.

## Design systems vs. component libraries

I often see the terms design system and component library used interchangeably. Although these two concepts are related, they pertain to different parts of a larger whole. 

In an effort to diffuse any confusion, I’ll define each term to point out the distinction and differences between the two related terminologies.

### Design systems

The [Nielsen Norman Group](https://www.nngroup.com/articles/design-systems-101/), a renowned and trusted UX research and consulting firm, defines a design system as a “…set of standards to manage design at scale by reducing redundancy while creating a shared language and visual consistency across different pages and channels.” 

A typical design system may consist of the following three items:

1.  Style guide: A standard of how elements should look and feel across a suite of products or websites. The most common type of style guide relates to brand design which involves elements like fonts, typography, colors, and logos
2.  Pattern library: Typically includes templates and layout structures intended to provide general workflow patterns; the patterns established are meant to be reused and adapted throughout an application
3.  Component library: Sometimes referred to as UI libraries or UI kits

Some examples of popular design systems include Google’s [Material Design](https://material.io/design/introduction), the [Atlassian Design System](https://atlassian.design/), and [Carbon Design System](https://carbondesignsystem.com/) by IBM.

### Component libraries

A [component library consists of a collection of UI elements](https://blog.logrocket.com/top-11-react-ui-libraries-kits/) (i.e., components) that can be reused across an application. These components implement the design standards established throughout the style guide. 

Due to their composability, components make it easier to implement wireframes provided by a design team. Common components found in a library range from buttons and individual form elements (e.g., inputs, checkboxes, etc.) to more complex elements like date pickers. 

Some examples of popular component libraries include [MUI](https://mui.com/core/) (the artist [formerly known as Material UI](https://mui.com/blog/material-ui-is-now-mui/)), [Headless UI](https://headlessui.com/) by Tailwind Labs, and of course [Radix UI](https://www.radix-ui.com/). 

Because a design system is a large entity that often involves multiple stakeholders across varying fields, building one is quite an arduous feat. In an effort to keep this article digestible, we will build out a component library as a subset of a design system. However, it is important to note that Radix offers a full suite of tools that provide you with the resources to build out full-fledged design systems.

## Installing and setting up the sample project

Documentation within a UI library is imperative because it provides a single location where developers and designers alike can view a list of the components within a design system. Developers can also leverage this documentation as a sandbox environment where they can test out components in multiple states and read up on the different properties that can be used to customize an individual component. 

Because documentation is so important, our sample project has been preconfigured with [React Styleguidist](https://react-styleguidist.js.org/), a development environment for building React components. We’ll use this tool to document the components as we build them out. 

To clone the [radix-component-library](https://github.com/Cool-Runningz/radix-component-library) project from GitHub, open your terminal and run the following command:

```rust
git clone https://github.com/Cool-Runningz/radix-component-library.git
```

Next, cd into the `radix-component-library` folder:

```rust
cd radix-component-library
```

Install the project dependencies defined in the `package.json` file:

```json
npm install
```

Once the installation is completed, start the development server:

```rust
npm run library
```

Navigate to http://localhost:3020/ in your browser. You should see the following screen: ![RangeInput](https://blog.logrocket.com/wp-content/uploads/2022/10/rangeinput.jpeg) 

Now that we have the project up and running, we can begin to incorporate Radix into our component library.

## Building a range input component with Radix

The first component we will build is a range input that adheres to the [WAI-ARIA slider design pattern](https://www.w3.org/WAI/ARIA/apg/patterns/slider/). 

Creating a custom range input that has a consistent appearance across browsers can be an arduous task. However, Radix makes this process simpler by providing a Slider Primitive. The range input is sometimes referred to as a slider; throughout the rest of this article, I will use these terms interchangeably. 

In Radix, each Primitive can be installed individually so you can adopt them incrementally. Primitives are also versioned independently, to further facilitate incremental adoption. This leads to less bloat in your codebases, as you only need to import the individual components that you need.

### Installation and boilerplate 

To build a range input component, start by installing the [Slider Primitive](https://www.radix-ui.com/docs/primitives/components/slider):

```rust
npm install @radix-ui/react-slider
```

Next, open the `RangeInput.jsx` file, import the Radix Primitive, and add the boilerplate for the slider component. At this point, your file should include the following code:

```javascript
import React from 'react'
import PropTypes from "prop-types"
import "./RangeInput.css"
import * as SliderPrimitive from '@radix-ui/react-slider';

const RangeInput = (props) => {
    return (
        <SliderPrimitive.Root className="rootSlider" value={props.value}>
            <SliderPrimitive.Track className="trackSlider">
                <SliderPrimitive.Range className="rangeSlider" />
            </SliderPrimitive.Track>
            <SliderPrimitive.Thumb className="thumbSlider"  />
        </SliderPrimitive.Root>
    )
}
export default RangeInput
```

The anatomy of a `Slider` component in Radix consists of the following four elements:

1.  [Root](https://www.radix-ui.com/docs/primitives/components/slider#root): Contains all the parts of a slider
2.  [Track](https://www.radix-ui.com/docs/primitives/components/slider#track): The track that contains the `Slider.Range`
3.  [Range](https://www.radix-ui.com/docs/primitives/components/slider#range): The range part that must be contained inside `Slider.Track`
4.  [Thumb](https://www.radix-ui.com/docs/primitives/components/slider#thumb): A draggable thumb

At this point, if you navigate to http://localhost:3020/#rangeinput you’ll notice that the UI is empty. This is because Radix components do not apply any styles by default. 

Next, we’ll go through the process of adding CSS to the custom component.

### Styling the component

Radix is not opinionated about how to style components and it is compatible with a variety of CSS solutions, providing you with complete control over styling. 

Using plain ol’ CSS, we’ll style to `RangeInput` component to match the one found in the “Estimate your cost - Professional” section of [LogRocket’s Pricing page](https://logrocket.com/pricing/): ![Slider](https://blog.logrocket.com/wp-content/uploads/2022/10/slider.jpeg)

#### `Root`

The `Root` contains all the parts of a slider and it will render an `input` for each thumb. 

Create a `.rootSlider` class in `RangeInput.css` and add the following code:

```css
 .rootSlider {
    display: flex;
    align-items: center;
    position: relative;
    height: 1.5rem;
    max-width: 30rem;
}
```

#### `Track`

The `Track` contains the `Slider.Range` and is the part of the slider that the thumb runs along. 

Create a `.trackSlider` class in `RangeInput.css` and add the following code:

```css
.trackSlider {
    background-color: #fafafa;
    border: 1px solid #d3d3d3;
    position: relative;
    flex-grow: 1;
    border-radius: 10px;
    height: 1rem;
}
```

#### `Range`

The `Range` must live inside `Slider.Track` and represents the range of values that is selected. 

Create a `.rangeSlider` class in `RangeInput.css` and add the following code:

```css
.rangeSlider {
    position: absolute;
    background: linear-gradient(90deg,#252678,#764abc);
    height: 100%;
    border-radius: 10px;
}
```

#### `Thumb`

The `Thumb` is the element on the track that the user can move around to select varying range values. 

Create a `.thumbSlider` class in `RangeInput.css` and add the following code:

```css
.thumbSlider {
    display: block;
    width: 20px;
    height: 20px;
    background-color: #764abc;
    border-radius: 30px;
    border: 1px solid #d9d9d9;
}

.thumbSlider:hover {
    cursor: pointer;
}

.thumbSlider:focus {
    outline: 1px solid white;
}
```

With all these styles applied, if you navigate back to http://localhost:3020/#rangeinput you should see the following being rendered: ![RangeInput CSS Applied](https://blog.logrocket.com/wp-content/uploads/2022/10/rangeinput-css-applied.jpeg)

### Adding props <a name="adding-props-1">

The last step of building the `RangeInput` component is to configure it to be able to accept props, which in turn will make it more reusable and customizable. For this example, we‘ll add three props:

1.  `label`: Applied as the [aria-label](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-label) value for the element with the [`slider` role](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/slider_role)
2.  `value`: Represents the controlled value of the slider and must be used in conjunction with `onValueChange`
3.  `onValueChange`: The event handler that gets called when the value changes

Radix components can be [controlled](https://reactjs.org/docs/forms.html#controlled-components) or [uncontrolled](https://reactjs.org/docs/uncontrolled-components.html). Adding the `value` and `onValueChange` props will turn the `RangeInput` into a controlled component. 

The markdown file that renders the `RangeInput` in the sandbox environment has been preconfigured to pass in props. Now we need to update the component to be able to handle those props. 

First, we need to add [typechecking with PropTypes](https://reactjs.org/docs/typechecking-with-proptypes.html). The [prop-types](https://www.npmjs.com/package/prop-types) library has already been imported near the top of `RangeInput.jsx` so now you can add the `propTypes` definition at the bottom of the file, like so:

```css
RangeInput.propTypes = {
    /** Applied as the aria-label value of the element with the "slider" role  */
    label: PropTypes.string.isRequired,
    /** The controlled value of the slider. */
    value: PropTypes.arrayOf(PropTypes.number).isRequired,
    /** Event handler called when the value changes. */
    onValueChange: PropTypes.func.isRequired
}
```

When `propTypes` are present, React Styleguidist will pick up the props from the object’s declarations and display them in a table. Now, anyone looking at this example will know the exact props that are needed to properly implement this component. ![React Styleguidist](https://blog.logrocket.com/wp-content/uploads/2022/10/react-styleguidist.jpeg) 

Next, we want to update the JSX within `RangeInput.jsx` to be able to pass the props to the `RangeInput` component. The `value` and `onValueChange` props need to get applied to the `SliderPrimitive.Root` element, while the `label` prop needs to get applied to the `SliderPrimitive.Thumb` element. 

With those updates, the `RangeInput.jsx` file should now contain the following code:

```javascript
import React from 'react'
import PropTypes from "prop-types"
import "./RangeInput.css"
import * as SliderPrimitive from '@radix-ui/react-slider';

const RangeInput = (props) => {
    return (
        <SliderPrimitive.Root className="rootSlider" 
           value={props.value} onValueChange={props.onValueChange}>
            <SliderPrimitive.Track className="trackSlider">
                <SliderPrimitive.Range className="rangeSlider" />
            </SliderPrimitive.Track>
            <SliderPrimitive.Thumb className="thumbSlider" aria-label={props.label} 
            />
        </SliderPrimitive.Root>
    )
}
export default RangeInput

RangeInput.propTypes = {
    /** Applied as the aria-label value of the element with the "slider" role  */
    label: PropTypes.string.isRequired,
    /** The controlled value of the slider. */
    value: PropTypes.arrayOf(PropTypes.number).isRequired,
    /** Event handler called when the value changes. */
    onValueChange: PropTypes.func.isRequired
}
```

Voila! 

With minimal code, we were able to leverage the [Radix Slider](https://www.radix-ui.com/docs/primitives/components/slider) to create an accessible and functional `RangeInput` component that is styled to match the LogRocket theme. 

Next, we’ll demonstrate how to implement one more component for our UI library.

## Building a tabs component with Radix

The second component we will build is a Tabs element that leverages the [Radix Tabs Primitive](https://www.radix-ui.com/docs/primitives/components/tabs). This component is constructed to adhere to the [WAI-ARIA Tabs design pattern](https://www.w3.org/WAI/ARIA/apg/patterns/tabpanel/).

### Installation and boilerplate <a name="installation-and-boilerplate-1">

First, install the Tabs Primitive:

```rust
npm install @radix-ui/react-tabs
```

Next, open the `Tabs.jsx` file, import the Radix Primitive, and add the boilerplate for the `Tabs` component. 

Your file should include the following code:

```rust
import React from 'react'
import PropTypes from "prop-types"
import "./Tabs.css"

import * as TabsPrimitive from "@radix-ui/react-tabs";

const Tabs = (props) => {
    return (
        <TabsPrimitive.Root>
            <TabsPrimitive.List>
                <TabsPrimitive.Trigger value="tab1">Tab 1</TabsPrimitive.Trigger>
                <TabsPrimitive.Trigger value="tab2">Tab 2</TabsPrimitive.Trigger>
            </TabsPrimitive.List>
            <TabsPrimitive.Content value="tab1">
                Content for Tab # 1
            </TabsPrimitive.Content>
            <TabsPrimitive.Content value="tab2">
                Content for Tab # 2
            </TabsPrimitive.Content>
        </TabsPrimitive.Root>
    )
}

export default Tabs
```

The anatomy of a Radix `Tabs` component consists of the following four elements:

1.  [Root](https://www.radix-ui.com/docs/primitives/components/tabs#root): Contains all the `Tabs` component parts
2.  [List](https://www.radix-ui.com/docs/primitives/components/tabs#list): Contains the triggers that are aligned along the edge of the active content
3.  [Trigger](https://www.radix-ui.com/docs/primitives/components/tabs#trigger): The button that activates its associated content
4.  [Content](https://www.radix-ui.com/docs/primitives/components/tabs#content): Contains the content associated with each trigger

If you navigate to http://localhost:3020/#tabs, the default version of the component should display like this: ![Tabs Without CSS](https://blog.logrocket.com/wp-content/uploads/2022/10/tabs-without-css.jpeg)

### Adding props

Next, we need to configure the `Tabs` component to be able to accept props. For this example, we’ll add three props:

1.  `tabsList`: Array of tabs to be displayed
2.  `value`: Controlled value of the tab to activate; this should be used in conjunction with `onValueChange`
3.  `onValueChange`: Event handler called when the value changes

Currently, the `Tabs` component only displays a fixed number of tabs with hardcoded values. In an effort to make the component more flexible so that it can render an arbitrary number of tabs, we will update the JSX in `Tabs.jsx` with the following code:

```javascript
import React from 'react'
import PropTypes from "prop-types"
import "./Tabs.css"
import * as TabsPrimitive from "@radix-ui/react-tabs";

const Tabs = (props) => {
    return (
        <TabsPrimitive.Root
            className="rootTabs"
            value={props.value}
            onValueChange={props.onValueChange}
        >
            <TabsPrimitive.List className="listTabs">
                {props.tabsList.map((tab) => {
                    return (
                        <TabsPrimitive.Trigger className="triggerTabs"
                            key={tab.id || `${index}-${tab.value}`}
                            value={tab.value}
                        >
                            {tab.label}
                        </TabsPrimitive.Trigger>);
                })}
            </TabsPrimitive.List>
            {props.tabsList.map((tab) => {
                return (
                    <TabsPrimitive.Content className="contentTabs"
                        key={tab.id || `${index}-${tab.value}`} 
                         value={tab.value}
                    >
                        {tab.content}
                    </TabsPrimitive.Content>
                );
            })}
        </TabsPrimitive.Root>
    )
}

export default Tabs

Tabs.propTypes = {
    tabsList: PropTypes.arrayOf(
        PropTypes.shape({
            id: PropTypes.string,
            label: PropTypes.string.isRequired,
            content: PropTypes.element.isRequired,
            value: PropTypes.string.isRequired
        })
    ),
    /** The controlled value of the tab to activate. */
    value: PropTypes.string.isRequired,
    /** Event handler called when the value changes. */
    onValueChange: PropTypes.func.isRequired
}
```

[Tabs.md](https://github.com/Cool-Runningz/radix-component-library/blob/main/src/components/Tabs.md) has been pre-configured to pass in an array of items to the `tabsList` prop. When we navigate to http://localhost:3020/#tabs, we should see the following: ![Tabs with Props](https://blog.logrocket.com/wp-content/uploads/2022/10/tabs-with-props.jpeg)

### Styling the component <a name="styling-the-component-1">

Similar to the previous component, we’ll use vanilla CSS to style the `Tabs` component to match the one found in the “Estimate your cost” section of [LogRocket’s Pricing page](https://logrocket.com/pricing/): ![Pricing Page](https://blog.logrocket.com/wp-content/uploads/2022/10/pricing-page.jpeg)

#### `Root`

The `Root` contains all the elements that are contained within the tabs component. 

Create a `.rootTabs` class in `Tabs.css` and add the following code:

```css
.rootTabs {
  background-color: white;
}
```

#### `List`

The `List` contains all the `Trigger` elements. 

Create a `.listTabs` class in `Tabs.css` and add the following code:

```css
.listTabs {
    align-items: center;
    display: flex;
    justify-content: center;
    margin: 0 auto;
    width: 85%;
}
```

#### `Trigger`

The `Trigger` is the button that controls displaying the selected tabs content. 

Create a `.triggerTabs` class in `Tabs.css` and add the following code:

```css
.triggerTabs {
    color: #764abc;
    font-family: "Avenir";
    background: none;
    border: none;
    display: block;
    cursor: pointer;
    font-size: 20px;
    font-weight: 500;
    opacity: 0.5;
    padding: 30px;
    position: relative;
}

.triggerTabs[data-state="active"] {
    opacity: 1;
}

.triggerTabs[data-state="active"]::after {
    background-color: #764abc;
    border-radius: 12px;
    bottom: -4px;
    content: "";
    height: 6px;
    left: 0;
    position: absolute;
    width: 100%;
}
```

#### `Content`

The `Content` contains the content that is associated with each tab. 

Create a `.contentTabs` class in `Tabs.css` and add the following CSS:

```css
.contentTabs {
   border-top: 3px solid hsla(0, 0%, 44%, 0.15);
   padding: 30px 55px;
}
```

With all those styles applied, if you navigate back to http://localhost:3020/#tabs you should now see the following being rendered: ![Tabs with CSS Applied](https://blog.logrocket.com/wp-content/uploads/2022/10/tabs-with-css-applied.jpeg) This completes the second example and shows how you can leverage the [Radix Tabs Primitive](https://www.radix-ui.com/docs/primitives/components/tabs) to quickly build a functional and accessible `Tabs` component that is styled to match the LogRocket theme.

## Conclusion

In this article, we investigated Radix and discussed how you can use its suite of products to create design systems. Building incrementally, we used Radix Primitives to develop a basic React component library as a subset of a design system. 

Developing custom components can be tricky, and incorporating features like accessibility and keyboard navigation can exponentially increase complexity, further validating the benefits of leveraging tools like Radix that aim to simplify this process. 

The repository for the component library developed in this article can be found on my [GitHub](https://github.com/Cool-Runningz/radix-component-library). In addition, you can [view a deployed version of the final project here](https://radix-ui-library.netlify.app). 

Thank you for reading, and happy coding!

---

## [LogRocket](https://logrocket.com/signup): Full visibility into production Next.js apps

Debugging Next applications can be difficult, especially when users experience issues that are hard to reproduce. If you’re interested in monitoring and tracking Redux state, automatically surfacing JavaScript errors, and tracking slow network requests and component load time, try [LogRocket](https://logrocket.com/signup). 

[![LogRocket signup](https://blog.logrocket.com/wp-content/uploads/2017/03/1d0cd-1s_rmyo6nbrasp-xtvbaxfg.png)](https://logrocket.com/signup)

[LogRocket](https://logrocket.com/signup) is like a DVR for web and mobile apps, recording literally everything that happens on your Next app. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app's performance, reporting with metrics like client CPU load, client memory usage, and more.

The LogRocket Redux middleware package adds an extra layer of visibility into your user sessions. LogRocket logs all actions and state from your Redux stores.

Modernize how you debug your Next.js apps — [start monitoring for free](https://logrocket.com/signup).