Yak

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:

  1. The compile time part
  2. The runtime part
  3. Optional: Context

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.

The compile time part

next-yak uses an SWC plugin and an additional webpack loader to transform your code. The plugin is responsible for transforming the usages of the tagged template literals (like styled and css), while the loader reads the results from the plugin, resolves cross module dependencies and writes the finished css in a css-modules file.

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.

Change import

The first step of the transformation is to change the import statement from styled and css to the next-yak runtime.

output
import { styled, css } 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 webpack loader at a later stage.

output
import __styleYak from "./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). In order that this import statement works 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 & PR's 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 it's loaders should be used.

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 references to the class names from the "not yet generated" css-modules file.

import { styled } from "next-yak";
 
const Button = styled.button`
  font-size: 2rem;
  font-weight: bold;
  color: blue;
  &:hover {
    color: red;
  }
`;

The dynamic parts are preserved and apply the classes or CSS variables at runtime. There are two different types of dynamic parts: The first type is the dynamic CSS property value (color in the following example) and the second type is the dynamic CSS property itself (background in the following example).

The property value is transformed into a CSS variable and set on the element itself directly based on the functions return value.

The property itself is transformed into a class name that is referenced during runtime when the function returns them.

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"}; //
	}
`;

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 webpack 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"};
	}
`;

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.

The webpack loader

See: css-loader.ts

The second loader takes the output of the first loader and resolves the cross module dependencies. The result is valid CSS that will be written to the context.

Resolve cross module dependencies

To resolve cross module dependencies, the first loader generates a unique string to signal the second loader that this is a dependency with a specific name. The second loader then reads the generated CSS from the SWC plugin and resolves the dependencies. Once the final css is generated the loader hands it over as a CSS module file so it can be processed by Next.js like a normal css-modules file.

The runtime part

See: styled.tsx

The runtime part is responsible for merging styles and props. It's a simple function that takes in the styles and the props and returns the merged styles.

A lot of this code is types and comments to indicate why certain things are done. The actual runtime code is very small.

Context

As of next.js >15.0.0, this feature doesn't work with server components anymore.

The yak theme context allows you to configure the theme of your yak application. It works for react-server components and normal react components.

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/jantimon/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/jantimon/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/jantimon/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'
import { cache } from "react";
 
const hasHighContrast = cache(() => {
    const cookieStore = cookies()
    return cookieStore.get("highContrast")?.value === "true"
});
 
export function getYakThemeContext() {
    return {
        highContrast: hasHighContrast()
    }
}
 
declare module "next-yak" {
    export interface YakTheme extends 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;
}

On this page