How does it work
This is for the curious minds who want to know how `next-yak` works under the hood and it's not necessary to understand it in order to use it.
Playground
A live playground to see the code transformation can be found here.
The basics
next-yak has 3 parts:
- The compile time part (SWC plugin + bundler integration)
- The runtime part (minimal class name merging)
- Optional: Context (theme provider)
The compile time part is responsible for extracting styles and transforming your code into something that uses the runtime part. The runtime part is responsible for merging styles and props.
next-yak supports three bundler backends: webpack (Next.js), Turbopack (Next.js), and Vite, each with its own integration layer. The SWC plugin is shared across all of them, while each bundler has a dedicated loader/plugin for CSS delivery.
The compile time part
next-yak uses a Rust-based SWC plugin and a bundler-specific loader to transform your code. The SWC plugin transforms the tagged template literals (like styled and css) and extracts CSS, while the loader resolves cross-module dependencies and delivers the CSS through the bundler's native CSS pipeline.
The SWC plugin
See: yak-swc
The SWC plugin takes in the source code and transforms the tagged template literals into next-yak runtime function
calls with the imported styles (from the final css module output) as arguments. It also transforms the dynamic parts of the
tagged template literals into function calls with the properties as argument and adds it as css value to the style object.
On top of that it resolves the css prop and folds static JSX usages of styled components directly into DOM elements.
Change import
The first step of the transformation is to change the import statement from styled and css to the next-yak runtime.
import * as __yak from "next-yak/internal";The normal import just references a kind of stub behavior to be used in tests without the need to have a transpilation step.
Add style import
The next step of the transformation is to add the import statement for the styles. These styles don't exist yet, but will be generated by the bundler-specific loader/plugin at a later stage. Since next-yak controls all generated class names (passing them as strings directly), this import does not need to export class name bindings. The import format depends on the bundler:
import "./page.yak.module.css!=!./page?./page.yak.module.css";This strange looking import string is a poorly documented feature of webpack. It uses the Inline matchResource to instrument webpack to use the loaders as if it was a normal .module.css file
that points to the current file (./page). For this import statement to work in Next.js, we need to add
a query parameter that ends with .module.css, as this is the way Next.js recognizes CSS modules.
This recognition is not always working correctly as seen with the various issues and PRs we created on the Next.js repository like this one, this one or this one.
So that means ./page.yak.module.css is a virtual file that is afterwards generated by the webpack loader and contains the
styles of the current file. ./page.yak.module.css!=!./page says that the loader (matching .yak.module.css files) should
be used with the current file as input. Lastly ?./page.yak.module.css is the query parameter that tells Next.js that this
is a CSS module and its loaders should be used.
import "data:text/css;base64,LkJ1dHRvbiB7Li4ufQ==";Turbopack uses a different approach than webpack. Instead of using inline match resources, the SWC plugin encodes the extracted CSS as base64 data URLs. This allows Turbopack to handle the CSS without needing the complex resource matching syntax that webpack uses.
import "virtual:yak-css:{{__MODULE_PATH__}}.css";For Vite, next-yak uses a virtual module system. The {{__MODULE_PATH__}} placeholder is replaced by the SWC plugin with the actual file path during transformation. The Vite plugin then resolves these virtual modules and serves the extracted CSS through Vite's standard CSS pipeline.
Transform tagged template literals
In another step, the tagged template literals are transformed into next-yak runtime function calls. The static CSS parts are passed as class name strings directly, since next-yak controls all generated class names.
import { styled } from "next-yak";
const Button = styled.button` font-size: 2rem;
font-weight: bold;
color: blue;
&:hover {
color: red;
}`;
import * as __yak from "next-yak/internal";
const Button = __yak.__yak_button(
"input_Button_m7uBBu"
);The dynamic parts are preserved and apply the classes or CSS variables at runtime. There are two types of dynamic parts:
Dynamic CSS property values are transformed into CSS variables. The function is preserved and its return value is set as an inline style on the element at runtime:
import { styled } from "next-yak";
const Button = styled.button` color: ${(props) => props.$primary
? "white"
: "blue"
};`;
import * as __yak from "next-yak/internal";
const Button = __yak.__yak_button(
"input_Button_m7uBBu",
{
"style": { "--input_Button__color_m7uBBu":
(props) => props.$primary
? "white"
: "blue",
},
}
);Dynamic CSS blocks (conditional styles using css) are extracted into separate class names that are toggled at runtime based on the function's return value:
import { styled, css } from "next-yak";
const Button = styled.button`
font-size: 2rem;
${(props) => props.$primary &&
css`
background: black;
`}
`;
import { css } from "next-yak/internal";
import * as __yak from "next-yak/internal";
const Button = __yak.__yak_button(
"input_Button_m7uBBu",
(props) => props.$primary &&
css("input_Button___m7uBBu"),
);Transform the css prop
The css prop is resolved at compile time as well. The styles are extracted like all other styles and the prop itself is replaced with a plain className attribute:
import { css } from "next-yak";
const Elem = () => (
<div
css={css`
color: red;
`}
/>
);const Elem = () => <div className="input_Elem_m7uBBu" />;Conditional styles stay conditional, the condition just toggles class names instead of css blocks:
const Elem = ({ on }) => (
<div
css={
on
? css`
color: red;
`
: css`
color: blue;
`
}
/>
);const Elem = ({ on }) => <div className={on ? "input_Elem_m7uBBu" : "input_Elem_m7uBBu-01"} />;Only if the element has an existing className attribute or spread attributes the class names have to be merged at render time. In that case the plugin wraps the attributes with the small __yak_mergeCssProp runtime helper.
Transform CSS
The next step is to extract the CSS to a comment in the source code. This is useful because the transformation should happen as quickly as possible and this is way easier and faster with the SWC plugin instead of the bundler loader and we're already transforming the current file.
import { styled, css } from "next-yak";
const Button = styled.button`
font-size: 2rem;
font-weight: bold;
color: ${props => props.$primary
? "white" : "blue"};
${props => props.$primary
? css`background: black;`
: css`background: white;` }
&:hover {
color: ${props => props.$primary
? "red" : "blue"};
}`;
/*YAK Extracted CSS:
.input_Button_m7uBBu {
font-size: 2rem;
font-weight: bold;
color: var(--input_Button__color_m7uBBu);
}
.input_Button___m7uBBu {
background: black;
}
.input_Button___m7uBBu1 {
background: white;
}
.input_Button_m7uBBu {
&:hover {
color: var(
--input_Button__color_m7uBBu1
);
}
}
*/The comment gets marked with /*YAK Extracted CSS: so that the loader can easily find it and extract the CSS.
Additionally, the comment is marked with /*#__PURE__*/ so that the minifier knows that behind the runtime function call there is no side effect and can move it around.
Fold static JSX usages
A styled component is itself a React component: when it renders it computes its class name, merges it with the received props and renders the actual DOM element. That is one extra component in the render tree for every usage. When a styled component is declared and used in the same file, the plugin already knows what it will render and folds the usage directly into the DOM element:
import { styled } from "next-yak";
const Card = styled.div`
color: red;
`;
const Page = () => <Card>Hello</Card>;const Card = __yak.__yak_div("input_Card_m7uBBu");
const Page = () => <div className="input_Card_m7uBBu">Hello</div>;React now renders one component less for this usage. Folding reaches further than fully static components: it inlines class-toggling props, collapses styled(Component) chains and folds the static css prop, and it keeps the runtime wrapper whenever it cannot prove the result is safe. See Folding for the full rules.
The bundler loaders/plugins
Each bundler has a dedicated loader or plugin that takes the SWC plugin output, resolves cross-module dependencies, and delivers the final CSS through the bundler's native CSS pipeline.
- Webpack (webpack-loader.ts): The loader reads the extracted CSS from the SWC plugin comments, resolves cross-module dependencies, and hands the final CSS over as a CSS module file so it can be processed by Next.js like a normal css-modules file.
- Turbopack (turbo-loader.ts): The loader works similarly to the webpack loader but the CSS is already encoded inline as a data URL by the SWC plugin, so no virtual file resolution is needed.
- Vite (vite-plugin.ts): The plugin resolves the virtual module imports and serves the extracted CSS through Vite's standard CSS pipeline.
The runtime part
See: styled.tsx
Despite being called "zero-runtime", next-yak does ship a small runtime. What "zero-runtime" means here is that there is no code that manipulates the DOM or CSSOM. No <style> or <link> elements are injected at runtime.
Folded JSX usages from the compile time step skip this runtime completely, they are plain DOM elements. The runtime only handles the usages that could not be folded.
The runtime is essentially a type-safe classnames utility. It takes the generated class name strings and the component's props, evaluates the dynamic functions (conditionals, CSS variable values), and returns:
- The final
classNamestring (merging any incomingclassName, base classes and conditional classes) - An optional
styleobject (setting CSS variable values from props)
Since it has no side effects, it works in both server and client components.
Here is a simplified version of what __yak.__yak_button does:
function __yak_button(className, ...dynamicParts) {
// returns a React component
return (props) => {
const classNames = new ClassNames(props.className);
classNames.add(className);
const style = {};
for (const part of dynamicParts) {
if (typeof part === "function") {
// conditional css block, e.g.:
// (props) => props.$primary && css("Button_primary")
// css("Button_primary") returns a function that
// adds "Button_primary" to the class names
const result = part(props);
if (result) result(props, classNames, style);
} else if (part?.style) {
// css variable values, e.g.:
// { style: { "--color": (props) => props.$color } }
for (const [key, value] of Object.entries(part.style)) {
style[key] = typeof value === "function" ? value(props) : value;
}
}
}
return <button className={classNames.value} style={style} />;
};
}When the component is rendered, the runtime passes the received props through all the dynamic functions, collects the resulting class names into a single string and CSS variable values into a style object, and forwards them as className and style props to the underlying DOM element.
Context
The yak theme context allows you to configure the theme of your yak application. It works for react-server components and normal react components, including full support for async server functions in Next.js 15+.
The first part of the puzzle is to export two different modules for the server and the client.
"./context": {
"react-server": {
"import": "./dist/context/index.server.js",
"require": "./dist/context/index.server.cjs"
},
"default": {
"import": "./dist/context/index.js",
"require": "./dist/context/index.cjs"
}
}For the client components, we use the default context from react.
"use client";
//
// This file is the client component (browser & ssr) version of index.server.tsx
//
import React, { ReactNode, createContext, useContext } from "react";
export interface YakTheme {}
/**
* The yak theme context
* @see https://github.com/DigitecGalaxus/next-yak/blob/main/packages/next-yak/runtime/context/README.md
*/
const YakContext = createContext<YakTheme>({});
/**
* Returns the current yak theme context
*
* @see https://github.com/DigitecGalaxus/next-yak/blob/main/packages/next-yak/runtime/context/README.md
*/
export const useTheme = (): YakTheme => useContext(YakContext);
/**
* Yak theme context provider
*
* @see https://github.com/DigitecGalaxus/next-yak/blob/main/packages/next-yak/runtime/context/README.md
*/
export const YakThemeProvider = ({
children,
theme = {},
}: {
children: ReactNode;
theme?: YakTheme;
}) => <YakContext.Provider value={theme}>{children}</YakContext.Provider>;For the server components, we use a module that exports exactly the same API as the one for client components, but with a different implementation where we use the cache function from react to cache the result of the context.
//
// This file is the react-server component version of index.tsx
//
// @ts-ignore - in the current @types/react "cache" is not typed
import React, { ReactNode, cache } from "react";
import { YakThemeProvider as YakThemeClientProvider } from "./index.js";
// the following import might be changed by
// the user config in withYak to point to their own
// context
import { getYakThemeContext } from "next-yak/context/baseContext";
export const useTheme = cache(() => getYakThemeContext());
export const YakThemeProvider = ({ children }: { children: ReactNode }) => {
return <YakThemeClientProvider theme={useTheme()}>{children}</YakThemeClientProvider>;
};The getYakThemeContext function is imported from the user's yak.context.ts file. This file is a regular typescript file that exports a function that returns the theme context. You can use every api that works with server components like headers or cookies and use them
to determine your values for the theme context.
import { cookies } from "next/headers";
export async function getYakThemeContext() {
const cookieStore = await cookies();
return {
highContrast: cookieStore.get("highContrast")?.value === "true",
};
}
declare module "next-yak" {
export interface YakTheme extends Awaited<ReturnType<typeof getYakThemeContext>> {}
}The second part of the puzzle is that the withYak function changes to what file webpack resolves the context so that
it points to the individual yak.context.ts file in the root directory.
// With the following alias the internal next-yak code
// is able to import a context which works for server components
const yakContext = resolveYakContext(yakOptions.contextPath, webpackConfig.context);
if (yakContext) {
webpackConfig.resolve.alias["next-yak/context/baseContext"] = yakContext;
}