Pigment CSS - new library from Material UI
First look at a new zero-runtime CSS-in-JS library from Material UI with examples
Bartłomiej Kozyra,
Daniel Chorągwicki
Multiple authors
2024-05-20
#Frontend
In this article
Overview
What is Pigment CSS
WyW-in-JS
Pigment CSS in practice
Our conclusions
Summary
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> );
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> );
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> );
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> );
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> //... ); };
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> ); };
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 });
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; }>; } }
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.
Bartłomiej Kozyra,
Daniel Chorągwicki
Multiple authors
Share this post
Related posts
Want to light up your ideas with us?
Our services
Skills