Pigment CSS - new library from Material UI

First look at a new zero-runtime CSS-in-JS library from Material UI with examples

2024-05-20

#Frontend

Overview

On the 16th of May, during the React Conf 2024 conference, Olivier Tassinari from the MUI team delivered a presentation titled "Pigment CSS, CSS in the server component age." In our article, we would like to delve into the topic of this library, which, although still in development, will officially debut in the new version of Material UI v6.

Developers creating applications with React typically utilise CSS-in-JS tools such as styled-components or Emotion for styling components. Both libraries focus on enhancing developer experiences by adding automatic prefixes (to improve compatibility with various browsers), providing style isolation, and improving code readability and debugging through self-generated component names.

As we know, technology is constantly evolving. Not long ago, we experienced the official release of Next.js 14, and for almost a month now, we've been familiarising ourselves with the beta version of React 19. Both in the new versions of Next.js and React, directives have been added, which we wrote about in our article React 19 has been officially announced! Aforementioned directives allow for generating components on the server or in the client (browser) side. Unfortunately, components styled using styled-components or Emotion are not compatible with server-side components. It is precisely this aspect, among others, and the desire to improve the performance of Material UI that prompted the creators to develop Pigment CSS library.

What is Pigment CSS

Pigment CSS is a zero-runtime CSS-in-JS library that offers support for Next.js and Vite. In the future, there are plans to extend support to other bundlers. It serves as an alternative to Emotion and styled-components, characterised by its zero-runtime solution. But what exactly is CSS-in-JS? It's an approach to styling web applications that involves writing CSS in JavaScript. Developers don't create separate CSS files; instead, they define styles directly in JavaScript code.

The goal of Pigment CSS creators is to shift the CSS processing time to build time. Compared to styled-components and Emotion, Pigment CSS aims to enhance DX (Developer Experience) by significantly improving performance and supporting the use of server components. However, nothing comes for free - this enhancement comes at the cost of increased compilation time.

The creators are aware of the number of projects built using styled-components and Emotion, so they have set themselves the goal of facilitating migration to the new tool. Especially in the case of transitioning from previous versions of Material UI based on Emotion to version v6, where Pigment CSS will be the standard. As the authors mention in their blog, working with RSC forced them to abandon familiar API interfaces, such as "useContext," which was used by Emotion, among others.

WyW-in-JS

Pigment CSS is built on top of WyW-in-JS (Whatever you want in JS). It's a toolkit that allows you to create your own CSS-in-JS libraries based on Zero-Runtime. It's worth noting that the tool is continuously evolving. WyW-in-JS provides developers with a comprehensive set of tools to create their own solutions with custom syntax and functionalities. The tool offers modules and plugins for popular packages such as Vite, Svelte, or Webpack, as well as the Next.js framework, ensuring compatibility with various compilation systems. It allows the use of any JavaScript in style definitions, loops, and conditions, as well as supports function calls and object literals, and tagged templates (a function that allows you to create template literals). You can find more information in WyW-in-JS documentation.

Pigment CSS in practice

Creating variants

Variants are a way to parameterise styles in Pigment CSS. It consists of an array of objects, where each object has props and style. Styles are applied to a given component when its props match the props object. Below is an example of a button that has a prop called size:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import { styled } from "@pigment-css/react";
 
export const ButtonWithVariants = styled("button")<{
    size: "default" | "small" | "large";
}>({
    backgroundColor: "transparent",
    border: "3px solid whitesmoke",
    borderRadius: "3px",
    color: "whitesmoke",
    cursor: "pointer",
    fontSize: "14px",
    outline: "none",
    overflow: "hidden",
    transition: "color 0.5s, transform 0.2s, background-color 0.2s",
    "&:hover": {
        backgroundColor: "whitesmoke",
        color: "#000000",
    },
    variants: [
        { props: { size: "default" }, style: { padding: "20px 30px" } },
        { props: { size: "small" }, style: { padding: "12px 18px" } },
        {
            props: { size: "large" },
            style: { fontSize: "18px", padding: "24px 34px" },
        },
    ],
});
 
export const PigmentVariantsExample = () => (
    <div
        className={css({
            alignItems: "center",
            display: "flex",
            flexDirection: "column",
            gap: "20px",
        })}
    >
        <ButtonWithVariants size="small">Small button</ButtonWithVariants>
        <ButtonWithVariants size="default">Default button</ButtonWithVariants>
        <ButtonWithVariants size="large">Large button</ButtonWithVariants>
    </div>
);

CodeSandbox example

Using CSS Selectors and overriding

CSS selectors are available as keys in the css and styled objects. Here are a few examples:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import { styled } from "@pigment-css/react";
 
const Text = styled("span")({
    fontSize: "16px",
});
 
export const Button = styled("button")({
    //...
    "&:hover": {
        color: "#000000",
    },
    "&::after, &::before": {
        borderRadius: "3px",
        content: '""',
        height: "100%",
        left: 0,
        position: "absolute",
        top: 0,
        width: "100%",
    },
    "&::before": {
        border: "3px solid #14b8a6",
        transition: "opacity 0.3s",
    },
    "&:hover::before": {
        opacity: 0,
    },
    //...
    [`${Text}`]: {
        fontSize: "14px",
    },
});
 
export const PigmentCssSelectors = () => (
    <div>
        <Button>
            <Text>Button</Text>
        </Button>
    </div>
);

CodeSandbox example

Additionally, Pigment CSS supports overriding previously created components:

1
2
3
4
5
6
7
8
9
const OverrideButton = styled(Button)({
    color: "#E3A008",
    "&::before": {
        border: "3px solid #E3A008",
    },
    "&::after": {
        backgroundColor: "#E3A008",
    },
});

Usage of media queries

Pigment CSS provides support for creating media queries in both css() and styled().

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { css, styled } from "@pigment-css/react";
 
const boxStyles = css({
    color: "#ffffff",
    textAlign: "center",
    "@media (max-width: 768px)": {
        color: "#5145CD",
    },
});
 
const Title = styled("h1")({
    fontSize: "32px",
    "@media (max-width: 768px)": {
        fontSize: "24px",
    },
});
 
export const PigmentMediaQueriesExample = () => (
    <div className={boxStyles}>
        <Title>Pigment media queries example</Title>
    </div>
);

CodeSandbox example

Animations using CSS keyframes

To create a reusable animation, we need to use the keyframes function. Calling this function is replaced by a unique animation name, which we can then place in the animation property.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
import { styled, keyframes } from "@pigment-css/react";
 
const circle = keyframes({
    "0%": {
        transform: "rotate(0deg)",
    },
    "100%": {
        transform: "rotate(360deg)",
    },
});
 
const motion = keyframes({
    "0%": {
        transform: "rotate(45deg)",
    },
    "100%": {
        transform: "rotate(405deg)",
    },
});
 
const Ring = styled("div")({
    position: "relative",
    background: "transparent",
    border: "3px solid #3c3c3c",
    borderRadius: "50%",
    color: "#60a5fa",
    height: "100px",
    width: "100px",
    "&::before": {
        animation: `${circle} 1.5s linear infinite`,
        border: "3px solid transparent",
        borderRadius: "50%",
        borderRight: "3px solid #38bdf8",
        borderTop: "3px solid #38bdf8",
        content: '""',
        height: "calc(100% + 6px)",
        left: "-3px",
        position: "absolute",
        top: "-3px",
        width: "calc(100% + 6px)",
    },
});
 
const Progress = styled("span")({
    animation: `${motion} 1.5s linear infinite`,
    background: "transparent",
    display: "block",
    height: "4px",
    left: "50%",
    position: "absolute",
    top: "calc(50% - 2px)",
    transformOrigin: "left",
    width: "50%",
    "&::before": {
        background: "#60a5fa",
        borderRadius: "50%",
        content: '""',
        height: "16px",
        position: "absolute",
        right: "-8px",
        top: "-6px",
        width: "16px",
    },
});
 
export const PigmentAnimationKeyframes = () => (
    <Ring>
        <Progress />
    </Ring>
);

CodeSandbox example

Component based on runtime values

Sometimes, we'll need styling based on values at runtime. For instance, when the value is based on user actions or is unknown because it's fetched from the backend. We have two ways to achieve this. The first is to use CSS variables directly in the style attribute:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const Count = styled("span")({
    color: "var(--color)",
    fontSize: "24px",
    transition: "color 0.5s",
});
 
const Counter = () => {
    const [color, setColor] = useState("white");
 
    return (
        //...
            <Count style={{ "--color": color } as CSSProperties}>{state}</Count>
        //...
    );
};

CodeSandbox example

The second method is to use a callback function for a specific CSS property. It's worth mentioning here that in this case, Pigment CSS replaces the callback with a CSS variable and injects the value using inline styles.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
const Box = styled("div")<{ color: string }>({
    alignItems: "center",
    border: ({ color }) => `3px solid ${color}`,
    display: "grid",
    gap: "24px",
    gridTemplateColumns: "auto 50px auto",
    justifyItems: "center",
    padding: "20px",
    transition: "border 0.5s",
});
 
const Counter = () => {
    const [color, setColor] = useState("white");
 
    return (
        <Box color={color}>
            {/* ... */}
        </Box>
    );
};
 
// Result
 
.by953dt {
    //...
    border: var(--by953dt-0);
}
 
<div
    style={{
        "--by953dt-0": `3px solid ${color}`;
    }}
>
    Hello
</div>

Theming

Theming is an optional feature that allows for the reuse of the same values, such as design tokens. Examples include colour palettes, typography or spacing. It's a simple object with any structure that we define in a configuration file. It's worth emphasising that the theme is added during the build and does not exist in the compiled JavaScript file. This allows us to benefit from theming in React Server Components.

An example of using theme in Next.js looks like this:

1
2
3
4
5
6
7
8
9
10
11
/** @type {import('next').NextConfig} */
const nextConfig = {};
 
module.exports = withPigment(nextConfig, {
    theme: {
        colors: {
            primary: "#2563eb",
            secondary: "#8b5cf6",
        },
    },
});

Access to the theme in the component is achieved through a callback which can be used with both styled() and css():

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { styled } from "@pigment-css/react";
 
const Title = styled("h2")(({ theme }) => ({
    color: theme.colors.secondary,
    fontSize: "24px",
}));
 
export const PigmentTheme = () => {
    return (
        <div>
            <Title>Themed title</Title>
        </div>
    );
};

CodeSandbox example

Pigment CSS provides us with a special utility called extendTheme which adds several features to our theme. One of them is generating the vars object, which contains tokens as references to CSS variables. Additionally, in the example below, cssVarPrefix has been used. It allows customising the name of CSS variables by adding a prefix.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/** @type {import('next').NextConfig} */
const nextConfig = {};
 
const theme = extendTheme({
    cssVarPrefix: "pigment-example",
    colors: {
        primary: "#2563eb",
        secondary: "#8b5cf6",
    },
});
 
console.log(theme.colors.primary); // #2563eb
console.log(theme.vars.colors.primary); // var(--pigment-example-colors-primary)
 
module.exports = withPigment(nextConfig, { theme });

CodeSandbox example

To use type checking for the theme, you need to extend the type ThemeArgs:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import type { ExtendTheme } from "@pigment-css/react/theme";
 
declare module "@pigment-css/react/theme" {
    interface ThemeTokens {
        colors: {
            primary: string;
            secondary: string;
        };
    }
 
    interface ThemeArgs {
        theme: ExtendTheme<{
            colorScheme: "light" | "dark";
            tokens: ThemeTokens;
        }>;
    }
}

CodeSandbox example

Our conclusions

Despite being in the early stages of development (currently 0.0.11), the Pigment CSS library is gaining popularity. This can be observed by tracking the increasing number of downloads of the tool from week to week. A huge advantage is that the tool was/is created by the developers of Material UI, which is downloaded millions of times each week. Therefore, there is definitely no reason to worry about support and development of the new library. Developers who have had the opportunity to download the library regularly report issues, which are analysed and addressed in subsequent releases. What we like the most is the emphasis on a well-thought-out migration from Emotion, which will not be as troublesome as it might have initially seemed. Programmers who have been using styled-components or Emotion so far will not have major issues switching to Pigment CSS because they are familiar with the main functions used to define styles (styled() and css()). At this point, the library has plugins for Next.js and Vite, as well as TypeScript support.

Referring to a blog post from Material UI, in which the creators tested the same application built on Next.js using Emotion and Pigment CSS, a significant difference in favour of Pigment CSS was observed:

  • 20% reduction in first load JavaScript (104 kB vs. 131 kB)
  • 15% reduction in Time To First Byte (381.5 ms vs 447.5 ms)
  • 9% decrease in First Contentful Paint (455 ms vs. 503 ms)
  • 7.5% reduction in page HTML (14.7 kB vs. 15.9 kB)

Summary

With the emergence of server components, there is a need to build new styling tools that can fully leverage the potential associated with performance improvements by shifting the rendering process of user interfaces to the server. Most existing tools struggle with this issue because their operation occurs on the client side.

In this article, we discussed one of the solutions to the server-side rendering problem, namely Pigment CSS from Material UI. Additionally, we built several examples using the mentioned library, discussed the migration process from Emotion to Pigment CSS, and shared our conclusions. Of course, there are other tools that support RSC, such as Tailwind CSS, Panda CSS, or StyleX. What sets them apart from Pigment CSS is the fact that they use atomic class names, which can make debugging code more challenging.

Whether the new library from Material UI will gain interest comparable to styled-components or Emotion remains to be seen later this year when it debuts alongside Material UI v6.

Our software development company specializes in innovative solutions in the field of Naxt.js and React technologies.

Share this post

Related posts

Frontend

2024-04-24

React 19 has been officially announced!

Frontend

2024-04-04

Next.js 14: Exploring fundamental concepts

Frontend

2023-09-20

Website Accessibility

Want to light up your ideas with us?

Józefitów 8, 30-039 Cracow, Poland

hidevanddeliver.com

(+48) 789 188 353

NIP: 9452214307

REGON: 368739409