Design Tokens Generator: design-tokens.dev Design Tokens with Emotion

Design Tokens integration with Emotion: 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

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

For this demonstration we’ll also be using Radix UI - 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';

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

Visually and conceptually Emotion is not much different from vanilla CSS, however it’s fully JS-compatible. Emotion offers several ways to style components (not only React), but using css props is the recommended one.

Let’s have a look at basic component. First, without any styling:

// excerpt form Header.tsx

const Header = (props: HeaderProps) => {
  const { children } = props;

  return (
    <header>
      <h1>
        Dystopian Weather
      </h1>
      <div>{children}</div>
    </header>
  );
};

And now let’s add Emotion:

// excerpt form Header.tsx

const Header = (props: HeaderProps) => {
  const { children } = props;

  return (
    <header
      css={css`
        position: relative;
        z-index: 20;

        display: flex;
        flex-flow: row nowrap;
        align-items: center;

        padding: var(--awsm-space-200) var(--awsm-space-100);
        border-bottom: var(--awsm-space-050) solid rgba(var(--awsm-color-primary-rgb), 0.5);

        background-color: var(--awsm-color-base-dark);
        color: var(--awsm-color-contrast-dark);
      `}
    >
      <h1
        css={css`
          flex: 1 1 auto;

          margin: 0;
        `}
      >
        Dystopian Weather
      </h1>
      <div>{children}</div>
    </header>
  );
};

Very straightforward! The only thing that’s changed is styles were added. Notice that we can effortlessly use Design Tokens already in the form of CSS variables.

Let’s now check the example with Radix UI component.
Same approach, let’s first inspect the structure:

// 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>
  );
};

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.
Source code for component can be found here.

// 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>
  );
};

Radix UI does not expose the states in components, so 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;
}
...

It’s worth to additionally mention that Emotion offers several methods for styling, and some might be more advanced, yet potentially more efficient for scalable UI architecture.

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 Emotion with Radix UI or another headless UI library with tokens created by Design Tokens Generator is simple, straightforward and beginner-friendly.
In many cases you don’t need to use a third party library, especially when design or project requirements are either simple or imply specific low-level customizations. Emotion can help in both situations, allowing you to create styled elements and compose them as needed.

Radix UI can be a great start for the UI needs of an application or even a custom UI library. Check out the respective guide if you wish to learn more.