Design Tokens integration with Radix UI: Tutorial and Example Application for Frontend Developers
It is recommended to check the intro article before diving in specifics of the current guide. Source code for this and other example applications can be found on Github.
Overview and Prerequisites
Radix UI offers unstyled, accessible components for building high‑quality design systems and web apps in React.
Radix UI allows granular installation of components.
For our needs we’ll need to install RadioGroup and Select:
npm install @radix-ui/react-radio-group
npm install @radix-ui/react-select
Later in code we’ll need to import components like that:
import * as RadioGroup from '@radix-ui/react-radio-group';
For this demonstration we’ll also be using Emotion as a styling solution. Emotion is a library designed for writing CSS styles with JavaScript. It provides powerful and predictable style composition in addition to a great developer experience with features such as source maps, labels, and testing utilities.
Install Emotion:
npm install @emotion/react
And that’s it! Let’s create our components!
Explore the source code in DTG Examples repository.
Current guide refers to the following dependencies versions, latest at the moment of writing:
"@dtg-examples/common-tokens": "1.0.0",
"@emotion/react": "11.11.1",
"@radix-ui/react-radio-group": "1.1.3",
"@radix-ui/react-select": "1.2.2",
"vite": "4.3.9"
Typical Component
We’ll be using Radix UI for creating two components for our app.
There are couple of reasons for that:
- Radix UI exposes only certain components, not focusing on generics like
Flex
orContainer
- for the needs of our app we don’t need extra elements
It’s worth noting that amount of exported components in Radix UI is x2+ higher than in Headless UI. This can be a deciding factor when choosing a headless React UI solution. Read more about Headless UI and Design Tokens in the respective guides.
The components that we’ll be adding are List (cities list) and Select (theme selector). Since they are very similar structure- and development-wise let’s overview only List.
// excerpt from List.tsx
const List = (props: ListProps) => {
const { clsx, name, value, items, onSelectValue } = props;
return (
<RadioGroup.Root
value={value}
onValueChange={onSelectValue}
name={name}
>
{items.map(({ uid, city, code, temp }) => (
<RadioGroup.Item key={uid} value={uid} asChild>
<div>
<div>
{city}
</div>
<div>
{weather[code]}: {temp}°C
</div>
</div>
</RadioGroup.Item>
))}
</RadioGroup.Root>
);
};
This looks good, too good to be true, actually. And your guess is right, since it’s a completely headless solution. The beauty of it is that it allows to comprehend the structure and function of component at a glance.
It’s worth noting that Radix UI does not expose the state of elements or sub-components, however it can be resolved with CSS. Let’s inspect the same component with styles implemented using Emotion:
// excerpt from List.tsx
const List = (props: ListProps) => {
const { clsx, name, value, items, onSelectValue } = props;
return (
<RadioGroup.Root
value={value}
onValueChange={onSelectValue}
name={name}
css={[
css`
display: flex;
flex-flow: column nowrap;
justify-items: stretch;
justify-content: stretch;
padding: var(--awsm-space-100);
background: var(--awsm-color-gamma-300);
`,
clsx,
]}
>
{items.map(({ uid, city, code, temp }) => (
<RadioGroup.Item key={uid} value={uid} asChild>
<div
css={css`
flex: 1 1 auto;
padding: var(--awsm-space-075) var(--awsm-space-100);
background: var(--awsm-color-secondary);
color: var(--awsm-color-secondary-contrast);
transition: all ease-out var(--awsm-duration-short);
cursor: pointer;
&[data-state='checked'] {
z-index: 1;
color: var(--awsm-color-secondary);
background: var(--awsm-color-secondary-contrast);
cursor: default;
}
@media (hover: hover) {
&:not([data-state='checked']):hover {
background: var(--awsm-color-secondary-tint);
}
}
&:focus-visible {
--focus-color: var(--awsm-color-primary);
}
`}
>
<div
css={css`
display: block;
font-size: var(--awsm-font-size-l);
font-weight: bold;
cursor: inherit;
`}
>
{city}
</div>
<div
css={css`
margin: 0;
font-size: var(--awsm-font-size-n);
`}
>
{weather[code]}: {temp}°C
</div>
</div>
</RadioGroup.Item>
))}
</RadioGroup.Root>
);
};
Complete code looks noticeably larger, but it’s the trade-off of using Emotion via css
prop. Emotion offers several methods for styling, and some might be more advanced, yet potentially more efficient for scalable UI architecture.
It’s worth mentioning that since Radix UI does not expose the states in components, it’s compensated by the respective data-*
attributes in the rendered html. Notice how styling of checked state is implemented for example:
...
&[data-state='checked'] {
z-index: 1;
color: var(--awsm-color-secondary);
background: var(--awsm-color-secondary-contrast);
cursor: default;
}
...
Design Tokens Integration
If you check the code in css{}
properties you won’t find something completely different from native CSS. As mentioned, Emotion offers couple of extra mechanics on top of traditional CSS, but essentially it’s the same code.
This allows us to use generated Design Tokens seamlessly:
<div
css={css`
display: block;
font-size: var(--awsm-font-size-l);
font-weight: bold;
cursor: inherit;
`}
>
{city}
</div>
Taking this up a notch, tokens can be mapped to a JS theme object and used even more flexibly:
// theme.js
const theme = {
...
fontSize: {
xs: 'var(--awsm-font-size-xs)',
s: 'var(--awsm-font-size-s)',
n: 'var(--awsm-font-size-n)',
l: 'var(--awsm-font-size-l)',
xl: 'var(--awsm-font-size-xl)',
xxl: 'var(--awsm-font-size-xxl)',
},
...
}
// component
<div
css={css`
display: block;
font-size: ${theme.fontSize.l};
font-weight: bold;
cursor: inherit;
`}
>
{city}
</div>
CSS variables that serve as design DNA for the application are installed via core styles.
The index.css
is imported at the application top level. The structure for index.css
and other styles can be found in the common project.
Conclusion
Using Radix UI with Emotion or other styling engine with tokens created by Design Tokens Generator is simple and straightforward.
Radix UI provides a number of out-of-the-box components that can be styled completely according to your design needs. Combined with Emotion powerful styling approach you get very lean, maintainable and scalable setup.
Radix UI can be a great start for the UI needs of an application or even a custom UI library. You can easily integrate a component or several components into your frontend codebase with just a couple of tweaks accounting for the component API.
If you need more examples for Emotion integration with Design Tokens, please check out the respective guide.