StyleX

The components are StyleX leaves. Use the tokens in your own leaves, compile with Vite or Next, or run no compiler at all.

Leaf to simplified output

Switch views to follow the concept from token references into layered atomic CSS.

const styles = stylex.create({
  panel: {
    padding: vlak.pad,
    color: vlak.ink,
    backgroundColor: vlak.paper,
  },
});

<section {...stylex.props(styles.panel)} />

Tokens stay as custom-property references in the leaf.

import * as stylex from "@stylexjs/stylex";
import { vlak, mq } from "@noorddev/vlak-react/tokens.stylex";

const styles = stylex.create({
  panel: {
    borderTopWidth: vlak.hairline,
    borderTopStyle: "solid",
    borderTopColor: vlak.divider,
    padding: vlak.pad,
    color: vlak.ink,
    backgroundColor: vlak.paper,
    [mq.phone]: { padding: 12 },
    [mq.reduce]: { transition: "none" },
  },
});

export function Panel(props: React.HTMLAttributes<HTMLDivElement>) {
  return <div {...props} {...stylex.props(styles.panel)} />;
}

vlak is a defineVars set that aliases the CSS custom properties; it is not a second scale. Change a token in CSS and the leaf follows. mq is a defineConsts set of media queries, so the compiler folds them and your breakpoints match the kit's.

vlak.*Aliases
paper, ink, gray--bg, --text, --text-secondary
divider, dividerSubtle, gridLine, tableAltthe four grays
controlBorder--control-border, 3:1 against the ground
radiusSm, radius, radiusChrome, radiusInthe radius family
pad, gutter, module--pad, --gutter, --grid-size
hit, controlH, controlFs, controlLabelthe control scale
durationSnap, duration, durationConfirm, ease, transitionmotion
textScale, hairline--text-scale and the literal 1px
mq.*Query
phonemax-width: 640px
mobileGridmax-width: 480px
at900, at899the rail boundary
railmin-width: 1024px
widemin-width: 1440px
reduceprefers-reduced-motion: reduce
touchhover: none
forcedColorsforced-colors: active

vlakFont and vlakMono export the two font stacks as strings.

StyleX resolves defineVars at compile time, so the file that defines them must pass through your compiler. @noorddev/vlak-react/tokens.stylex ships uncompiled for that reason; its hashes match the package's own compiled leaves. The rest of the package is already compiled and needs nothing.

// vite.config.ts
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import stylex from "@stylexjs/unplugin";

export default defineConfig({
  plugins: [
    // Finds packages that depend on @stylexjs/stylex, @noorddev/vlak-react
    // included, and compiles their StyleX along with yours.
    stylex.vite({ useCSSLayers: true }),
    react(),
  ],
});

@stylexjs/unplugin discovers installed packages that depend on @stylexjs/stylex and transforms them. If a package is missed, list it under externalPackages. The compiled CSS is appended to your bundle's CSS asset.

// next.config.mjs (what vlak.dev does)
// A Babel pre-loader runs @stylexjs/babel-plugin on app/ and components/
// only. SWC keeps everything else. Vlak arrives precompiled from
// node_modules, so it is not in the include list.
const stylexBabelOptions = {
  dev: false,
  runtimeInjection: false,
  treeshakeCompensation: true,
  unstable_moduleResolution: { type: "commonJS", rootDir: process.cwd() },
};

const stylexRule = (test, isTSX) => ({
  test,
  enforce: "pre",
  include: [path.join(here, "app"), path.join(here, "components")],
  use: [{
    loader: "babel-loader",
    options: {
      babelrc: false,
      configFile: false,
      plugins: [
        ["@babel/plugin-syntax-typescript", { isTSX }],
        "@babel/plugin-syntax-jsx",
        ["@stylexjs/babel-plugin", stylexBabelOptions],
      ],
    },
  }],
});

export default {
  output: "export",
  webpack: (config) => {
    config.module.rules.unshift(stylexRule(/\.tsx$/, true), stylexRule(/\.ts$/, false));
    return config;
  },
};
// postcss.config.cjs: extracts the compiled CSS into the file that holds "@stylex;"
module.exports = {
  plugins: {
    "@stylexjs/postcss-plugin": {
      include: ["app/**/*.{js,jsx,ts,tsx}", "components/**/*.{js,jsx,ts,tsx}"],
      babelConfig: {
        babelrc: false,
        parserOpts: { plugins: ["typescript", "jsx"] },
        plugins: [["@stylexjs/babel-plugin", stylexBabelOptions]],
      },
      useCSSLayers: false,
    },
  },
};

/* app/stylex.css */
@stylex;

Two halves: a Babel pass that rewrites stylex.* calls, and the PostCSS plugin that collects the CSS. Both read the same options so class hashes match. This site compiles its own leaves this way and consumes Vlak precompiled. The @stylexjs/nextjs-plugin package is the shorter road if you are not on a static export.

import "@noorddev/vlak-react/css";
import { Button } from "@noorddev/vlak-react";

The package works with no StyleX toolchain at all: the leaves are compiled at publish time and the stylesheet carries the result. Add a compiler only when you write leaves of your own.

npx @noorddev/vlak-cli add button dialog

components/vlak/
  button.tsx          the leaf: stylex.create + rs-* classes
  dialog.tsx
  cx.ts, rs.ts        shared helpers, installed once
  tokens.stylex.ts    the same vars, local to your compile
styles/vlak/
  button.css          the CSS projection, if you would rather not compile

add copies the source leaf, its registry dependencies, and the shared helpers once. From there the file is yours: edit it, and your compiler owns the output. Vendored leaves import a local tokens.stylex.ts, so the same rules about including it in the compile apply. The CSS projection lands next to it for pages that skip React.