Skip to content

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.

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 two webpack loaders to transform your code. The first loader is responsible for transforming the usages of the tagged template literals (like styled and css) and the second loader is responsible for transforming the styles into real CSS.

The first loader (tsloader.cjs)

The first loader takes in the source code and transforms the tagged template literals into next-yak runtime function calls with the imported styles (from the second loader) 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.

Add style import

The first 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 second loader.

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.

So that means ./page.yak.module.css is a virtual file that is generated by the second loader and contains the styles of the current file. ./page.yak.module.css!=!./page says that the second 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

As the next 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 module 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"};
	}
`;

The second loader (cssloader.cjs)

The second loader runs after the first loader changed the import, takes in the styles and transforms it into real CSS inside CSS-Modules. It also generates a class name for each static and dynamic style in order to reference it in the first loader. It translates dynamic property values into CSS variables and adds it to the class selector.

Remove imports

The first step of the transformation is to remove all import statements as the input is the normal JS file and we need to output a CSS file.

Transform static styles

The static styles are transformed into CSS classes. The class names are generated based on the component name and referenced in the first loader.

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

Transform dynamic styles

The dynamic property styles are transformed into nested :where selectors that don't alter the specificity of the selector. The class names are generated based on the condition of the properties and used in the first loader. For dynamic property values, the value is transformed into a CSS variable.

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 runtime part

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.

See: Styled function

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

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.

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.

client: index.tsx
"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.

server: index.server.tsx
//
// 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.

yak.context.ts
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.

withYak/index.ts
// 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;
}