StyleX
The components are StyleX leaves. Use the tokens in your own leaves, compile with Vite or Next, or run no compiler at all.
Writing a 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, tableAlt | the four grays |
controlBorder | --control-border, 3:1 against the ground |
radiusSm, radius, radiusChrome, radiusIn | the radius family |
pad, gutter, module | --pad, --gutter, --grid-size |
hit, controlH, controlFs, controlLabel | the control scale |
durationSnap, duration, durationConfirm, ease, transition | motion |
textScale, hairline | --text-scale and the literal 1px |
| mq.* | Query |
|---|---|
phone | max-width: 640px |
mobileGrid | max-width: 480px |
at900, at899 | the rail boundary |
rail | min-width: 1024px |
wide | min-width: 1440px |
reduce | prefers-reduced-motion: reduce |
touch | hover: none |
forcedColors | forced-colors: active |
vlakFont and vlakMono export the two font stacks as strings.
Include the tokens in your compile
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
// 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.js
// 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.
No compiler
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.
Vendoring leaves
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.