Design Tokens Generator: design-tokens.dev Design Tokens with Radix UI

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 or Container
  • 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.