diff --git a/packages/@stylexjs/atoms/README.md b/packages/@stylexjs/atoms/README.md
new file mode 100644
index 000000000..f95651266
--- /dev/null
+++ b/packages/@stylexjs/atoms/README.md
@@ -0,0 +1,72 @@
+# @stylexjs/atoms
+
+Compile-time helpers for authoring StyleX atomic styles.
+
+This package exposes CSS properties as a namespaced object and lets you express
+static and dynamic styles using normal JavaScript syntax. There is no new
+runtime styling system and no design tokens — everything compiles to the same
+output as stylex.create.
+
+The compiler treats atomic styles from this package as if they were authored
+locally, enabling the same optimizations as normal StyleX styles.
+
+## Usage
+
+```js
+import * as stylex from '@stylexjs/stylex';
+import x from '@stylexjs/atoms';
+
+function Example({ color }) {
+ return (
+
+ );
+}
+```
+
+### Static values
+
+Static styles are expressed via property access and are fully resolved at
+compile time.
+
+```js
+x.display.flex;
+x.flexDirection.column;
+```
+
+#### Values starting with numbers
+
+Use a leading underscore for values that begin with a number. The underscore is
+ignored by the compiler and has no semantic meaning.
+
+```js
+x.padding._16px;
+x.fontSize._1rem;
+```
+
+### Complex literal values
+
+For values that are not valid JavaScript identifiers (for example, values that
+contain spaces or symbols), use computed property syntax.
+
+```js
+x.fontSize['1.25rem'];
+x.width['calc(100% - 20cqi)'];
+x.gridTemplateColumns['1fr minmax(0, 3fr)'];
+```
+
+### Dynamic values
+
+Dynamic styles use call syntax and should be used sparingly.
+
+```js
+x.color(color);
+x.marginLeft(offset);
+```
diff --git a/packages/@stylexjs/atoms/package.json b/packages/@stylexjs/atoms/package.json
new file mode 100644
index 000000000..708d71b27
--- /dev/null
+++ b/packages/@stylexjs/atoms/package.json
@@ -0,0 +1,26 @@
+{
+ "name": "@stylexjs/atoms",
+ "version": "0.17.5",
+ "description": "Atomic style helpers for StyleX.",
+ "license": "MIT",
+ "main": "src/index.js",
+ "types": "src/index.d.ts",
+ "exports": {
+ ".": "./src/index.js",
+ "./babel-transform": "./src/babel-transform.js"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/facebook/stylex.git"
+ },
+ "sideEffects": false,
+ "peerDependencies": {
+ "@stylexjs/stylex": "^0.17.5"
+ },
+ "dependencies": {
+ "csstype": "^3.1.3"
+ },
+ "files": [
+ "src"
+ ]
+}
diff --git a/packages/@stylexjs/atoms/src/babel-transform.js b/packages/@stylexjs/atoms/src/babel-transform.js
new file mode 100644
index 000000000..1b67584f0
--- /dev/null
+++ b/packages/@stylexjs/atoms/src/babel-transform.js
@@ -0,0 +1,232 @@
+/**
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+'use strict';
+
+const t = require('@babel/types');
+
+const ATOMS_SOURCE = '@stylexjs/atoms';
+
+/**
+ * Creates a visitor that transforms utility style expressions into raw style objects.
+ * The babel-plugin will handle compilation via stylexCreate.
+ *
+ * - css.display.flex -> { display: 'flex' }
+ * - css.color(myVar) -> { color: myVar }
+ *
+ * @param {object} state - StateManager from babel-plugin
+ * @returns {object} Babel visitor
+ */
+function createUtilityStylesVisitor(state) {
+ return {
+ MemberExpression(path) {
+ if (path.node._utilityStyleProcessed) {
+ return;
+ }
+
+ const staticStyle = getStaticStyleFromPath(path, state);
+ if (staticStyle != null) {
+ path.node._utilityStyleProcessed = true;
+ transformStaticStyle(path, staticStyle);
+ }
+ },
+
+ CallExpression(path) {
+ if (path.node._utilityStyleProcessed) {
+ return;
+ }
+
+ const dynamicStyle = getDynamicStyleFromPath(path, state);
+ if (dynamicStyle != null) {
+ path.node._utilityStyleProcessed = true;
+ transformDynamicStyle(path, dynamicStyle);
+ }
+ },
+ };
+}
+
+function isUtilityStylesIdentifier(ident, state, path) {
+ if (state.inlineCSSImports && state.inlineCSSImports.has(ident.name)) {
+ return true;
+ }
+
+ const binding = path.scope?.getBinding(ident.name);
+ if (!binding) {
+ return false;
+ }
+
+ if (
+ binding.path.isImportSpecifier() &&
+ binding.path.parent.type === 'ImportDeclaration' &&
+ binding.path.parent.source.value === ATOMS_SOURCE
+ ) {
+ return true;
+ }
+
+ if (
+ binding.path.isImportNamespaceSpecifier() &&
+ binding.path.parent.type === 'ImportDeclaration' &&
+ binding.path.parent.source.value === ATOMS_SOURCE
+ ) {
+ return true;
+ }
+
+ if (
+ binding.path.isImportDefaultSpecifier() &&
+ binding.path.parent.type === 'ImportDeclaration' &&
+ binding.path.parent.source.value === ATOMS_SOURCE
+ ) {
+ return true;
+ }
+
+ return false;
+}
+
+function getPropKey(prop, computed) {
+ if (!computed && t.isIdentifier(prop)) {
+ return prop.name;
+ }
+ if (computed && t.isStringLiteral(prop)) {
+ return prop.value;
+ }
+ if (computed && t.isNumericLiteral(prop)) {
+ return String(prop.value);
+ }
+ return null;
+}
+
+function normalizeValue(value) {
+ if (typeof value === 'string' && value.startsWith('_')) {
+ return value.slice(1);
+ }
+ return value;
+}
+
+function getStaticStyleFromPath(path, state) {
+ const node = path.node;
+ if (!t.isMemberExpression(node)) {
+ return null;
+ }
+
+ if (path.parentPath?.isCallExpression() && path.parentPath.node.callee === node) {
+ return null;
+ }
+
+ const valueKey = getPropKey(node.property, node.computed);
+ if (valueKey == null) {
+ return null;
+ }
+
+ const parent = node.object;
+
+ if (t.isMemberExpression(parent)) {
+ const propName = getPropKey(parent.property, parent.computed);
+ const base = parent.object;
+ if (
+ propName != null &&
+ t.isIdentifier(base) &&
+ isUtilityStylesIdentifier(base, state, path)
+ ) {
+ return { property: propName, value: normalizeValue(valueKey) };
+ }
+ }
+
+ if (t.isIdentifier(parent) && isUtilityStylesIdentifier(parent, state, path)) {
+ const importedName = state.inlineCSSImports?.get(parent.name) ?? 'color';
+ const property = importedName === '*' ? valueKey : importedName;
+ return { property, value: normalizeValue(valueKey) };
+ }
+
+ return null;
+}
+
+function getDynamicStyleFromPath(path, state) {
+ const callee = path.get('callee');
+ if (!callee.isMemberExpression()) {
+ return null;
+ }
+
+ const valueKey = getPropKey(callee.node.property, callee.node.computed);
+ if (valueKey == null) {
+ return null;
+ }
+
+ if (path.node.arguments.length !== 1) {
+ return null;
+ }
+
+ const argPath = path.get('arguments')[0];
+ if (!argPath || !argPath.node || !argPath.isExpression()) {
+ return null;
+ }
+
+ const parent = callee.node.object;
+
+ if (t.isIdentifier(parent) && isUtilityStylesIdentifier(parent, state, path)) {
+ return {
+ property: valueKey,
+ value: argPath.node,
+ };
+ }
+
+ if (t.isMemberExpression(parent)) {
+ const propName = getPropKey(parent.property, parent.computed);
+ const base = parent.object;
+ if (
+ propName != null &&
+ t.isIdentifier(base) &&
+ isUtilityStylesIdentifier(base, state, path)
+ ) {
+ return {
+ property: propName,
+ value: argPath.node,
+ };
+ }
+ }
+
+ return null;
+}
+
+/**
+ * Transform static utility style to raw style object
+ * css.display.flex -> { display: 'flex' }
+ */
+function transformStaticStyle(path, styleInfo) {
+ const { property, value } = styleInfo;
+
+ const styleObj = t.objectExpression([
+ t.objectProperty(
+ t.stringLiteral(property),
+ typeof value === 'number'
+ ? t.numericLiteral(value)
+ : t.stringLiteral(String(value)),
+ ),
+ ]);
+
+ path.replaceWith(styleObj);
+}
+
+/**
+ * Transform dynamic utility style to raw style object
+ * css.color(myVar) -> { color: myVar }
+ */
+function transformDynamicStyle(path, styleInfo) {
+ const { property, value } = styleInfo;
+
+ const styleObj = t.objectExpression([
+ t.objectProperty(t.stringLiteral(property), value),
+ ]);
+
+ path.replaceWith(styleObj);
+}
+
+module.exports = {
+ createUtilityStylesVisitor,
+ isUtilityStylesIdentifier,
+ getStaticStyleFromPath,
+ getDynamicStyleFromPath,
+};
diff --git a/packages/@stylexjs/atoms/src/index.d.ts b/packages/@stylexjs/atoms/src/index.d.ts
new file mode 100644
index 000000000..9b0bfe2b2
--- /dev/null
+++ b/packages/@stylexjs/atoms/src/index.d.ts
@@ -0,0 +1,44 @@
+/**
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+import type { StyleXStyles } from '@stylexjs/stylex';
+import type { Properties } from 'csstype';
+
+/**
+ * Static atom access returns CompiledStyles compatible with stylex.props
+ * e.g., css.display.flex -> { $$css: true, display: 'x78zum5' }
+ */
+type StaticAtom = StyleXStyles<{ [key: string]: V }>;
+
+/**
+ * Dynamic atom call returns StyleXStyles with inline styles
+ * e.g., css.color(myVar) -> [{ $$css: true, color: 'x14rh7hd' }, { '--x-color': myVar }]
+ */
+type DynamicAtom = (value: V) => StyleXStyles<{ [key: string]: V }>;
+
+/**
+ * Each CSS property provides both static access and dynamic function call
+ */
+type AtomProperty = {
+ [Key in string | number]: StaticAtom;
+} & DynamicAtom;
+
+type CSSValue = string | number;
+
+/**
+ * The atoms namespace provides access to all CSS properties.
+ * All properties are defined (non-optional) to avoid undefined checks.
+ */
+type Atoms = {
+ [Key in keyof Properties]-?: AtomProperty<
+ NonNullable[Key]>
+ >;
+};
+
+declare const atoms: Atoms;
+
+export = atoms;
diff --git a/packages/@stylexjs/atoms/src/index.js b/packages/@stylexjs/atoms/src/index.js
new file mode 100644
index 000000000..5f69533b3
--- /dev/null
+++ b/packages/@stylexjs/atoms/src/index.js
@@ -0,0 +1,35 @@
+/**
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+'use strict';
+
+const valueProxy = (_propName) =>
+ new Proxy(function () {}, {
+ get() {
+ return valueProxy('');
+ },
+ apply() {
+ return undefined;
+ },
+ });
+
+const utilityStyles = new Proxy(function () {}, {
+ get(_target, prop) {
+ if (typeof prop === 'string') {
+ return valueProxy(prop);
+ }
+ return undefined;
+ },
+ apply() {
+ throw new Error(
+ '@stylexjs/atoms is a compile-time helper. Attempted to call it as a function, but the StyleX compiler did not run.',
+ );
+ },
+});
+
+module.exports = utilityStyles;
+module.exports.default = utilityStyles;
diff --git a/packages/@stylexjs/babel-plugin/__tests__/transform-stylex-props-test.js b/packages/@stylexjs/babel-plugin/__tests__/transform-stylex-props-test.js
index cffa3edd3..c92228794 100644
--- a/packages/@stylexjs/babel-plugin/__tests__/transform-stylex-props-test.js
+++ b/packages/@stylexjs/babel-plugin/__tests__/transform-stylex-props-test.js
@@ -231,6 +231,501 @@ describe('@stylexjs/babel-plugin', () => {
});
});
+ describe('props calls with atoms', () => {
+ test('inline static styles match stylex.create', () => {
+ const inline = transform(`
+ import stylex from 'stylex';
+ import * as css from '@stylexjs/atoms';
+ stylex.props(css.display.flex);
+ `);
+ expect(inline).toMatchInlineSnapshot(`
+ "import _inject from "@stylexjs/stylex/lib/stylex-inject";
+ var _inject2 = _inject;
+ import stylex from 'stylex';
+ import * as css from '@stylexjs/atoms';
+ _inject2({
+ ltr: ".x78zum5{display:flex}",
+ priority: 3000
+ });
+ ({
+ className: "x78zum5"
+ });"
+ `);
+ const local = transform(`
+ import stylex from 'stylex';
+ const styles = stylex.create({
+ flex: { display: 'flex' },
+ });
+ stylex.props(styles.flex);
+ `);
+ expect(local).toMatchInlineSnapshot(`
+ "import _inject from "@stylexjs/stylex/lib/stylex-inject";
+ var _inject2 = _inject;
+ import stylex from 'stylex';
+ _inject2({
+ ltr: ".x78zum5{display:flex}",
+ priority: 3000
+ });
+ ({
+ className: "x78zum5"
+ });"
+ `);
+ });
+
+ test('inline static supports leading underscore value', () => {
+ const inline = transform(`
+ import stylex from 'stylex';
+ import * as css from '@stylexjs/atoms';
+ stylex.props(css.padding._16px);
+ `);
+ expect(inline).toMatchInlineSnapshot(`
+ "import _inject from "@stylexjs/stylex/lib/stylex-inject";
+ var _inject2 = _inject;
+ import stylex from 'stylex';
+ import * as css from '@stylexjs/atoms';
+ _inject2({
+ ltr: ".x1tamke2{padding:16px}",
+ priority: 1000
+ });
+ ({
+ className: "x1tamke2"
+ });"
+ `);
+ const local = transform(`
+ import stylex from 'stylex';
+ const styles = stylex.create({
+ pad: { padding: '16px' },
+ });
+ stylex.props(styles.pad);
+ `);
+ expect(local).toMatchInlineSnapshot(`
+ "import _inject from "@stylexjs/stylex/lib/stylex-inject";
+ var _inject2 = _inject;
+ import stylex from 'stylex';
+ _inject2({
+ ltr: ".x1tamke2{padding:16px}",
+ priority: 1000
+ });
+ ({
+ className: "x1tamke2"
+ });"
+ `);
+ });
+
+ test('inline static supports computed key syntax', () => {
+ const inline = transform(`
+ import stylex from 'stylex';
+ import * as css from '@stylexjs/atoms';
+ stylex.props(css.width['calc(100% - 20px)']);
+ `);
+ expect(inline).toMatchInlineSnapshot(`
+ "import _inject from "@stylexjs/stylex/lib/stylex-inject";
+ var _inject2 = _inject;
+ import stylex from 'stylex';
+ import * as css from '@stylexjs/atoms';
+ _inject2({
+ ltr: ".xnlsq7q{width:calc(100% - 20px)}",
+ priority: 4000
+ });
+ ({
+ className: "xnlsq7q"
+ });"
+ `);
+ const local = transform(`
+ import stylex from 'stylex';
+ const styles = stylex.create({
+ w: { width: 'calc(100% - 20px)' },
+ });
+ stylex.props(styles.w);
+ `);
+ expect(inline).toMatchInlineSnapshot(`
+ "import _inject from "@stylexjs/stylex/lib/stylex-inject";
+ var _inject2 = _inject;
+ import stylex from 'stylex';
+ import * as css from '@stylexjs/atoms';
+ _inject2({
+ ltr: ".xnlsq7q{width:calc(100% - 20px)}",
+ priority: 4000
+ });
+ ({
+ className: "xnlsq7q"
+ });"
+ `);
+ expect(local).toMatchInlineSnapshot(`
+ "import _inject from "@stylexjs/stylex/lib/stylex-inject";
+ var _inject2 = _inject;
+ import stylex from 'stylex';
+ _inject2({
+ ltr: ".xnlsq7q{width:calc(100% - 20px)}",
+ priority: 4000
+ });
+ ({
+ className: "xnlsq7q"
+ });"
+ `);
+ });
+
+ test('inline css supports named imports', () => {
+ const inline = transform(`
+ import stylex from 'stylex';
+ import { color } from '@stylexjs/atoms';
+ stylex.props(color.blue);
+ `);
+ expect(inline).toMatchInlineSnapshot(`
+ "import _inject from "@stylexjs/stylex/lib/stylex-inject";
+ var _inject2 = _inject;
+ import stylex from 'stylex';
+ import { color } from '@stylexjs/atoms';
+ _inject2({
+ ltr: ".xju2f9n{color:blue}",
+ priority: 3000
+ });
+ ({
+ className: "xju2f9n"
+ });"
+ `);
+ });
+
+ test('dedupes duplicate properties across create and atoms', () => {
+ const output = transform(`
+ import stylex from 'stylex';
+ import * as css from '@stylexjs/atoms';
+ const styles = stylex.create({
+ base: { color: 'red', backgroundColor: 'white' },
+ });
+ stylex.props(styles.base, css.color.blue, css.backgroundColor.white);
+ `);
+ expect(output).toMatchInlineSnapshot(`
+ "import _inject from "@stylexjs/stylex/lib/stylex-inject";
+ var _inject2 = _inject;
+ import stylex from 'stylex';
+ import * as css from '@stylexjs/atoms';
+ _inject2({
+ ltr: ".x1e2nbdu{color:red}",
+ priority: 3000
+ });
+ _inject2({
+ ltr: ".x12peec7{background-color:white}",
+ priority: 3000
+ });
+ _inject2({
+ ltr: ".xju2f9n{color:blue}",
+ priority: 3000
+ });
+ _inject2({
+ ltr: ".x12peec7{background-color:white}",
+ priority: 3000
+ });
+ ({
+ className: "xju2f9n x12peec7"
+ });"
+ `);
+ });
+
+ test('dynamic style', () => {
+ const inline = transform(`
+ import stylex from 'stylex';
+ import * as css from '@stylexjs/atoms';
+ stylex.props(css.color(color));
+ `);
+ const local = transform(`
+ import stylex from 'stylex';
+ const styles = stylex.create({
+ color: (c) => ({ color: c }),
+ });
+ stylex.props(styles.color(color));
+ `);
+ expect(local).toMatchInlineSnapshot(`
+ "import _inject from "@stylexjs/stylex/lib/stylex-inject";
+ var _inject2 = _inject;
+ import stylex from 'stylex';
+ _inject2({
+ ltr: ".x14rh7hd{color:var(--x-color)}",
+ priority: 3000
+ });
+ _inject2({
+ ltr: "@property --x-color { syntax: \\"*\\"; inherits: false;}",
+ priority: 0
+ });
+ const styles = {
+ color: c => [{
+ kMwMTN: c != null ? "x14rh7hd" : c,
+ $$css: true
+ }, {
+ "--x-color": c != null ? c : undefined
+ }]
+ };
+ stylex.props(styles.color(color));"
+ `);
+ expect(inline).toMatchInlineSnapshot(`
+ "import _inject from "@stylexjs/stylex/lib/stylex-inject";
+ var _inject2 = _inject;
+ import stylex from 'stylex';
+ import * as css from '@stylexjs/atoms';
+ _inject2({
+ ltr: ".x14rh7hd{color:var(--x-color)}",
+ priority: 3000
+ });
+ _inject2({
+ ltr: "@property --x-color { syntax: \\"*\\"; inherits: false;}",
+ priority: 0
+ });
+ const _temp = {
+ color: _v => [{
+ "kMwMTN": _v != null ? "x14rh7hd" : _v,
+ "$$css": true
+ }, {
+ "--x-color": _v != null ? _v : undefined
+ }]
+ };
+ stylex.props(_temp.color(color));"
+ `);
+ });
+
+ test('inline static with inline dynamic', () => {
+ const output = transform(`
+ import stylex from 'stylex';
+ import * as css from '@stylexjs/atoms';
+ stylex.props(css.display.flex, css.color(color));
+ `);
+ expect(output).toContain('.x78zum5{display:flex}');
+ expect(output).toContain('.x14rh7hd{color:var(--x-color)}');
+ expect(output).toContain('--x-color');
+ expect(output).toContain('_v != null ? "x14rh7hd" : _v');
+ });
+
+ test('inline static with create dynamic', () => {
+ const output = transform(`
+ import stylex from 'stylex';
+ import * as css from '@stylexjs/atoms';
+ const styles = stylex.create({
+ opacity: (o) => ({ opacity: o }),
+ });
+ stylex.props(css.display.flex, styles.opacity(0.5));
+ `);
+ expect(output).toContain('.x78zum5{display:flex}');
+ expect(output).toContain('.xb4nw82{opacity:var(--x-opacity)}');
+ expect(output).toContain('--x-opacity');
+ });
+
+ test('inline dynamic with create dynamic', () => {
+ const output = transform(`
+ import stylex from 'stylex';
+ import * as css from '@stylexjs/atoms';
+ const styles = stylex.create({
+ opacity: (o) => ({ opacity: o }),
+ });
+ stylex.props(css.color(color), styles.opacity(0.5));
+ `);
+ expect(output).toMatchInlineSnapshot(`
+ "import _inject from "@stylexjs/stylex/lib/stylex-inject";
+ var _inject2 = _inject;
+ import stylex from 'stylex';
+ import * as css from '@stylexjs/atoms';
+ _inject2({
+ ltr: ".xb4nw82{opacity:var(--x-opacity)}",
+ priority: 3000
+ });
+ _inject2({
+ ltr: "@property --x-opacity { syntax: \\"*\\"; inherits: false;}",
+ priority: 0
+ });
+ const styles = {
+ opacity: o => [{
+ kSiTet: o != null ? "xb4nw82" : o,
+ $$css: true
+ }, {
+ "--x-opacity": o != null ? o : undefined
+ }]
+ };
+ _inject2({
+ ltr: ".x14rh7hd{color:var(--x-color)}",
+ priority: 3000
+ });
+ _inject2({
+ ltr: "@property --x-color { syntax: \\"*\\"; inherits: false;}",
+ priority: 0
+ });
+ const _temp = {
+ color: _v => [{
+ "kMwMTN": _v != null ? "x14rh7hd" : _v,
+ "$$css": true
+ }, {
+ "--x-color": _v != null ? _v : undefined
+ }]
+ };
+ stylex.props(_temp.color(color), styles.opacity(0.5));"
+ `);
+ expect(output).toContain('.x14rh7hd{color:var(--x-color)}');
+ expect(output).toContain('.xb4nw82{opacity:var(--x-opacity)}');
+ expect(output).toContain('--x-color');
+ expect(output).toContain('--x-opacity');
+ });
+
+ test('inline static + inline dynamic coexist', () => {
+ const inline = transform(`
+ import stylex from 'stylex';
+ import * as css from '@stylexjs/atoms';
+ stylex.props(css.display.flex, css.color(color));
+ `);
+ expect(inline).toMatchInlineSnapshot(`
+ "import _inject from "@stylexjs/stylex/lib/stylex-inject";
+ var _inject2 = _inject;
+ import stylex from 'stylex';
+ import * as css from '@stylexjs/atoms';
+ _inject2({
+ ltr: ".x78zum5{display:flex}",
+ priority: 3000
+ });
+ _inject2({
+ ltr: ".x14rh7hd{color:var(--x-color)}",
+ priority: 3000
+ });
+ _inject2({
+ ltr: "@property --x-color { syntax: \\"*\\"; inherits: false;}",
+ priority: 0
+ });
+ const _temp = {
+ color: _v => [{
+ "kMwMTN": _v != null ? "x14rh7hd" : _v,
+ "$$css": true
+ }, {
+ "--x-color": _v != null ? _v : undefined
+ }]
+ };
+ const _temp2 = {
+ k1xSpc: "x78zum5",
+ $$css: true
+ };
+ stylex.props(_temp2, _temp.color(color));"
+ `);
+ });
+
+ test('inline static + create dynamic', () => {
+ const output = transform(`
+ import stylex from 'stylex';
+ import * as css from '@stylexjs/atoms';
+ const styles = stylex.create({
+ opacity: (o) => ({ opacity: o }),
+ });
+ stylex.props(css.display.flex, styles.opacity(0.5));
+ `);
+ expect(output).toMatchInlineSnapshot(`
+ "import _inject from "@stylexjs/stylex/lib/stylex-inject";
+ var _inject2 = _inject;
+ import stylex from 'stylex';
+ import * as css from '@stylexjs/atoms';
+ _inject2({
+ ltr: ".xb4nw82{opacity:var(--x-opacity)}",
+ priority: 3000
+ });
+ _inject2({
+ ltr: "@property --x-opacity { syntax: \\"*\\"; inherits: false;}",
+ priority: 0
+ });
+ const styles = {
+ opacity: o => [{
+ kSiTet: o != null ? "xb4nw82" : o,
+ $$css: true
+ }, {
+ "--x-opacity": o != null ? o : undefined
+ }]
+ };
+ _inject2({
+ ltr: ".x78zum5{display:flex}",
+ priority: 3000
+ });
+ const _temp = {
+ k1xSpc: "x78zum5",
+ $$css: true
+ };
+ stylex.props(_temp, styles.opacity(0.5));"
+ `);
+ });
+
+ test('inline dynamic + create dynamic', () => {
+ const output = transform(`
+ import stylex from 'stylex';
+ import * as css from '@stylexjs/atoms';
+ const styles = stylex.create({
+ opacity: (o) => ({ opacity: o }),
+ });
+ stylex.props(css.color(color), styles.opacity(0.5));
+ `);
+ expect(output).toMatchInlineSnapshot(`
+ "import _inject from "@stylexjs/stylex/lib/stylex-inject";
+ var _inject2 = _inject;
+ import stylex from 'stylex';
+ import * as css from '@stylexjs/atoms';
+ _inject2({
+ ltr: ".xb4nw82{opacity:var(--x-opacity)}",
+ priority: 3000
+ });
+ _inject2({
+ ltr: "@property --x-opacity { syntax: \\"*\\"; inherits: false;}",
+ priority: 0
+ });
+ const styles = {
+ opacity: o => [{
+ kSiTet: o != null ? "xb4nw82" : o,
+ $$css: true
+ }, {
+ "--x-opacity": o != null ? o : undefined
+ }]
+ };
+ _inject2({
+ ltr: ".x14rh7hd{color:var(--x-color)}",
+ priority: 3000
+ });
+ _inject2({
+ ltr: "@property --x-color { syntax: \\"*\\"; inherits: false;}",
+ priority: 0
+ });
+ const _temp = {
+ color: _v => [{
+ "kMwMTN": _v != null ? "x14rh7hd" : _v,
+ "$$css": true
+ }, {
+ "--x-color": _v != null ? _v : undefined
+ }]
+ };
+ stylex.props(_temp.color(color), styles.opacity(0.5));"
+ `);
+ });
+
+ describe('with options', () => {
+ test('dev/debug classnames for atoms', () => {
+ const inline = transform(
+ `
+ import stylex from 'stylex';
+ import * as css from '@stylexjs/atoms';
+ stylex.props(css.display.flex);
+ `,
+ {
+ dev: true,
+ debug: true,
+ enableDevClassNames: true,
+ enableDebugClassNames: true,
+ filename: '/tmp/Foo.js',
+ },
+ );
+ expect(inline).toMatchInlineSnapshot(`
+ "import _inject from "@stylexjs/stylex/lib/stylex-inject";
+ var _inject2 = _inject;
+ import stylex from 'stylex';
+ import * as css from '@stylexjs/atoms';
+ _inject2({
+ ltr: ".display-x78zum5{display:flex}",
+ priority: 3000
+ });
+ ({
+ className: "Foo____inline__ display-x78zum5"
+ });"
+ `);
+ });
+ });
+ });
+
test('stylex call with number', () => {
expect(
transform(`
diff --git a/packages/@stylexjs/babel-plugin/src/index.js b/packages/@stylexjs/babel-plugin/src/index.js
index 7b4648a01..9ec62f850 100644
--- a/packages/@stylexjs/babel-plugin/src/index.js
+++ b/packages/@stylexjs/babel-plugin/src/index.js
@@ -38,6 +38,8 @@ import {
LOGICAL_FLOAT_END_VAR,
} from './shared/preprocess-rules/legacy-expand-shorthands';
import transformStyleXDefineMarker from './visitors/stylex-define-marker';
+import { createUtilityStylesVisitor } from '@stylexjs/atoms/babel-transform';
+import { convertObjectToAST } from './utils/js-to-ast';
const NAME = 'stylex';
@@ -160,6 +162,14 @@ function styleXTransform(): PluginObj<> {
skipStylexPropsChildren(path, state);
},
});
+ // Run atoms visitor first to transform x.prop.value patterns
+ // This runs BEFORE stylex.props so that atomic styles are already
+ // compiled when stylex.props processes them
+ const atomsVisitor = createUtilityStylesVisitor(state, {
+ convertObjectToAST,
+ });
+ path.traverse(atomsVisitor);
+
path.traverse({
CallExpression(path: NodePath) {
transformStylexCall(path, state);
diff --git a/packages/@stylexjs/babel-plugin/src/utils/state-manager.js b/packages/@stylexjs/babel-plugin/src/utils/state-manager.js
index f9d7ccbcb..1ee398610 100644
--- a/packages/@stylexjs/babel-plugin/src/utils/state-manager.js
+++ b/packages/@stylexjs/babel-plugin/src/utils/state-manager.js
@@ -13,6 +13,7 @@ import type {
CompiledNamespaces,
StyleXOptions as RuntimeOptions,
} from '../shared';
+import type { FlatCompiledStyles } from '../shared/common-types';
import type { Check } from './validate';
import * as t from '@babel/types';
@@ -165,12 +166,20 @@ export default class StateManager {
+stylexViewTransitionClassImport: Set = new Set();
+stylexDefaultMarkerImport: Set = new Set();
+stylexWhenImport: Set = new Set();
+ // Map of local identifier -> imported name.
+ // For namespace/default imports we store '*'.
+ +inlineCSSImports: Map = new Map();
injectImportInserted: ?t.Identifier = null;
// `stylex.create` calls
+styleMap: Map = new Map();
+styleVars: Map> = new Map();
+ +inlineStylesCache: Map = new Map();
+ +inlineDynamicCache: Map<
+ string,
+ { className: string, varName: string, classKey: string },
+ > = new Map();
// results of `stylex.create` calls that should be kept
+styleVarsToKeep: Set<[string, true | string, true | Array]> =
diff --git a/packages/@stylexjs/babel-plugin/src/visitors/imports.js b/packages/@stylexjs/babel-plugin/src/visitors/imports.js
index 784e98f24..e344315e8 100644
--- a/packages/@stylexjs/babel-plugin/src/visitors/imports.js
+++ b/packages/@stylexjs/babel-plugin/src/visitors/imports.js
@@ -12,6 +12,8 @@ import type { NodePath } from '@babel/traverse';
import * as t from '@babel/types';
import StateManager from '../utils/state-manager';
+const INLINE_CSS_SOURCES = new Set(['@stylexjs/inline-css']);
+
// Read imports of react and remember the name of the local variables for later
export function readImportDeclarations(
path: NodePath,
@@ -22,6 +24,28 @@ export function readImportDeclarations(
return;
}
const sourcePath = node.source.value;
+
+ if (INLINE_CSS_SOURCES.has(sourcePath)) {
+ for (const specifier of node.specifiers) {
+ if (specifier.type === 'ImportNamespaceSpecifier') {
+ state.inlineCSSImports.set(specifier.local.name, '*');
+ } else if (specifier.type === 'ImportDefaultSpecifier') {
+ state.inlineCSSImports.set(specifier.local.name, '*');
+ } else if (
+ specifier.type === 'ImportSpecifier' &&
+ (specifier.imported.type === 'Identifier' ||
+ specifier.imported.type === 'StringLiteral')
+ ) {
+ const importedName =
+ specifier.imported.type === 'Identifier'
+ ? specifier.imported.name
+ : specifier.imported.value;
+ state.inlineCSSImports.set(specifier.local.name, importedName);
+ }
+ }
+ return;
+ }
+
if (state.importSources.includes(sourcePath)) {
for (const specifier of node.specifiers) {
if (
@@ -97,6 +121,9 @@ export function readImportDeclarations(
if (importedName === 'defaultMarker') {
state.stylexDefaultMarkerImport.add(localName);
}
+ if (importedName === 'css') {
+ state.inlineCSSImports.set(localName, '*');
+ }
}
}
}
@@ -126,6 +153,24 @@ export function readRequires(
return;
}
state.importPaths.add(importPath);
+ if (INLINE_CSS_SOURCES.has(importPath)) {
+ if (node.id.type === 'Identifier') {
+ state.inlineCSSImports.set(node.id.name, '*');
+ return;
+ }
+ if (node.id.type === 'ObjectPattern') {
+ for (const prop of node.id.properties) {
+ if (
+ prop.type === 'ObjectProperty' &&
+ prop.key.type === 'Identifier' &&
+ prop.value.type === 'Identifier'
+ ) {
+ state.inlineCSSImports.set(prop.value.name, prop.key.name);
+ }
+ }
+ return;
+ }
+ }
if (node.id.type === 'Identifier') {
state.stylexImport.add(node.id.name);
}
@@ -179,6 +224,34 @@ export function readRequires(
if (prop.key.name === 'defaultMarker') {
state.stylexDefaultMarkerImport.add(value.name);
}
+ if (prop.key.name === 'css') {
+ state.inlineCSSImports.set(value.name, '*');
+ }
+ }
+ }
+ }
+ }
+
+ if (
+ init != null &&
+ init.type === 'CallExpression' &&
+ init.callee?.type === 'Identifier' &&
+ init.callee?.name === 'require' &&
+ init.arguments?.length === 1 &&
+ init.arguments?.[0].type === 'StringLiteral' &&
+ INLINE_CSS_SOURCES.has(init.arguments[0].value)
+ ) {
+ if (node.id.type === 'Identifier') {
+ state.inlineCSSImports.set(node.id.name, '*');
+ }
+ if (node.id.type === 'ObjectPattern') {
+ for (const prop of node.id.properties) {
+ if (
+ prop.type === 'ObjectProperty' &&
+ prop.key.type === 'Identifier' &&
+ prop.value.type === 'Identifier'
+ ) {
+ state.inlineCSSImports.set(prop.value.name, prop.key.name);
}
}
}
diff --git a/packages/@stylexjs/babel-plugin/src/visitors/stylex-props.js b/packages/@stylexjs/babel-plugin/src/visitors/stylex-props.js
index d32378994..3c0466e50 100644
--- a/packages/@stylexjs/babel-plugin/src/visitors/stylex-props.js
+++ b/packages/@stylexjs/babel-plugin/src/visitors/stylex-props.js
@@ -16,13 +16,17 @@ import { props } from '@stylexjs/stylex';
import { convertObjectToAST } from '../utils/js-to-ast';
import { evaluate } from '../utils/evaluate-path';
import stylexDefaultMarker from '../shared/stylex-defaultMarker';
+import styleXCreateSet from '../shared/stylex-create';
+import { hoistExpression } from '../utils/ast-helpers';
+import { injectDevClassNames } from '../utils/dev-classname';
type ClassNameValue = string | null | boolean | NonStringClassNameValue;
type NonStringClassNameValue = [t.Expression, ClassNameValue, ClassNameValue];
-type StyleObject = {
- [key: string]: string | null | boolean,
-};
+type StyleObject = $ReadOnly<{
+ [key: string]: string | null,
+ $$css?: true | string,
+}>;
class ConditionalStyle {
test: t.Expression;
@@ -102,6 +106,12 @@ export default function transformStylexProps(
disableImports: true,
};
+ // Pre-process: compile any raw style objects (from atoms) before the main loop
+ // This transforms { property: 'value' } into { propKey: 'className', $$css: true }
+ for (const argPath of argsPath) {
+ compileRawStyleObjects(argPath, state, evaluatePathFnConfig);
+ }
+
const resolvedArgs: ResolvedArgs = [];
for (const argPath of argsPath) {
currentIndex++;
@@ -109,11 +119,14 @@ export default function transformStylexProps(
if (
argPath.isObjectExpression() ||
argPath.isIdentifier() ||
- argPath.isMemberExpression()
+ argPath.isMemberExpression() ||
+ argPath.isCallExpression()
) {
const resolved = parseNullableStyle(argPath, state, evaluatePathFnConfig);
if (resolved === 'other') {
- bailOutIndex = currentIndex;
+ if (bailOutIndex == null) {
+ bailOutIndex = currentIndex;
+ }
bailOut = true;
} else {
resolvedArgs.push(resolved);
@@ -135,7 +148,9 @@ export default function transformStylexProps(
evaluatePathFnConfig,
);
if (primary === 'other' || fallback === 'other') {
- bailOutIndex = currentIndex;
+ if (bailOutIndex == null) {
+ bailOutIndex = currentIndex;
+ }
bailOut = true;
} else {
resolvedArgs.push(new ConditionalStyle(test, primary, fallback));
@@ -162,7 +177,9 @@ export default function transformStylexProps(
evaluatePathFnConfig,
);
if (leftResolved !== 'other' || rightResolved === 'other') {
- bailOutIndex = currentIndex;
+ if (bailOutIndex == null) {
+ bailOutIndex = currentIndex;
+ }
bailOut = true;
} else {
resolvedArgs.push(
@@ -171,14 +188,16 @@ export default function transformStylexProps(
conditional++;
}
} else {
- bailOutIndex = currentIndex;
+ if (bailOutIndex == null) {
+ bailOutIndex = currentIndex;
+ }
bailOut = true;
}
if (conditional > 4) {
bailOut = true;
}
if (bailOut) {
- break;
+ continue;
}
}
if (!state.options.enableInlinedConditionalMerge && conditional) {
@@ -263,6 +282,32 @@ export default function transformStylexProps(
MemberExpression,
});
}
+
+ // Hoist inline CSS objects (from atoms) to module level
+ // Raw objects are pre-compiled in compileRawStyleObjects, so this only hoists compiled objects
+ // eslint-disable-next-line no-inner-declarations
+ function ObjectExpression(objPath: NodePath) {
+ // Check if this object has $$css: true (compiled utility style object)
+ const hasCssMarker = objPath.node.properties.some(
+ (prop) =>
+ t.isObjectProperty(prop) &&
+ ((t.isIdentifier(prop.key) && prop.key.name === '$$css') ||
+ (t.isStringLiteral(prop.key) && prop.key.value === '$$css')) &&
+ t.isBooleanLiteral(prop.value, { value: true }),
+ );
+ if (hasCssMarker) {
+ const hoisted = hoistExpression(objPath, objPath.node);
+ objPath.replaceWith(hoisted);
+ }
+ }
+
+ if (argPath.isObjectExpression()) {
+ ObjectExpression(argPath);
+ } else {
+ argPath.traverse({
+ ObjectExpression,
+ });
+ }
}
} else {
path.skip();
@@ -331,6 +376,19 @@ function parseNullableStyle(
return null;
}
+ // Local dynamic style functions (e.g., styles.opacity(1)) should bail out
+ // so runtime props merging keeps the conditional class/inline var semantics.
+ if (path.isCallExpression()) {
+ const callee = path.get('callee');
+ if (callee.isMemberExpression()) {
+ const obj = callee.get('object');
+ if (obj.isIdentifier() && state.styleMap.has(obj.node.name)) {
+ return 'other';
+ }
+ }
+ return 'other';
+ }
+
if (t.isMemberExpression(node)) {
const { object, property, computed: computed } = node;
let objName = null;
@@ -358,8 +416,14 @@ function parseNullableStyle(
if (objName != null && propName != null) {
const style = state.styleMap.get(objName);
if (style != null && style[String(propName)] != null) {
+ const memberVal = style[String(propName)];
+ // Dynamic style functions (arrow/function expressions) should bail out
+ // so runtime props handling remains intact.
+ if (typeof memberVal === 'function') {
+ return 'other';
+ }
// $FlowFixMe[incompatible-type]
- return style[String(propName)];
+ return memberVal;
}
}
}
@@ -479,3 +543,218 @@ function isCalleeMemberExpression(
state.stylexImport.has(node.callee.object.name)
);
}
+
+/**
+ * Pre-compile raw style objects from atoms.
+ * This transforms { property: 'value' } into { propKey: 'className', $$css: true }
+ * and registers the CSS injection.
+ */
+function compileRawStyleObjects(
+ argPath: NodePath<>,
+ state: StateManager,
+ evaluatePathFnConfig: FunctionConfig,
+): void {
+ function processObjectExpression(objPath: NodePath) {
+ // Check if this object already has $$css marker (already compiled)
+ const hasCssMarker = objPath.node.properties.some(
+ (prop) =>
+ t.isObjectProperty(prop) &&
+ ((t.isIdentifier(prop.key) && prop.key.name === '$$css') ||
+ (t.isStringLiteral(prop.key) && prop.key.value === '$$css')),
+ );
+ if (hasCssMarker) {
+ return;
+ }
+
+ // Empty objects don't need processing
+ const properties = objPath.node.properties;
+ if (properties.length === 0) {
+ return;
+ }
+
+ // Try to evaluate the object
+ const evalResult = evaluate(objPath, state, evaluatePathFnConfig);
+
+ // Check if the object has dynamic values (non-evaluatable)
+ if (!evalResult.confident) {
+ // Check if this is a simple {property: dynamicValue} pattern
+ if (properties.length === 1 && t.isObjectProperty(properties[0])) {
+ const prop = properties[0];
+ const key = t.isIdentifier(prop.key) && !prop.computed
+ ? prop.key.name
+ : t.isStringLiteral(prop.key)
+ ? prop.key.value
+ : null;
+
+ if (key != null && t.isExpression(prop.value) && !t.isLiteral(prop.value)) {
+ // This is a dynamic style: { property: dynamicValue }
+ compileDynamicStyle(objPath, key, prop.value, state);
+ return;
+ }
+ }
+ return;
+ }
+
+ if (evalResult.value == null) {
+ return;
+ }
+
+ // Skip proxies
+ if (evalResult.value.__IS_PROXY === true) {
+ return;
+ }
+
+ // This is a raw style object - compile it
+ const rawStyle = evalResult.value;
+
+ // Create a namespace wrapper for styleXCreateSet: { __inline__: rawStyle }
+ const styleInput = { __inline__: rawStyle };
+
+ // eslint-disable-next-line prefer-const
+ let [compiledStyles, injectedStyles] = styleXCreateSet(
+ styleInput,
+ state.options,
+ );
+
+ // Inject dev class names if enabled
+ if (state.isDev && state.opts.enableDevClassNames) {
+ compiledStyles = {
+ ...injectDevClassNames(compiledStyles, null, state),
+ };
+ }
+
+ // Register the CSS
+ const listOfStyles = Object.entries(injectedStyles).map(
+ ([key, { priority, ...rest }]) => [key, rest, priority],
+ );
+ state.registerStyles(listOfStyles, objPath);
+
+ // Get the compiled style for __inline__ namespace
+ const compiledStyle = compiledStyles.__inline__;
+ if (compiledStyle == null) {
+ return;
+ }
+
+ // Convert to AST and replace
+ const compiledAst = convertObjectToAST(compiledStyle);
+ objPath.replaceWith(compiledAst);
+ }
+
+ if (argPath.isObjectExpression()) {
+ processObjectExpression(argPath);
+ } else if (argPath.isConditionalExpression()) {
+ const consequentPath = argPath.get('consequent');
+ const alternatePath = argPath.get('alternate');
+ if (consequentPath.isObjectExpression()) {
+ processObjectExpression(consequentPath);
+ }
+ if (alternatePath.isObjectExpression()) {
+ processObjectExpression(alternatePath);
+ }
+ } else if (argPath.isLogicalExpression()) {
+ const rightPath = argPath.get('right');
+ if (rightPath.isObjectExpression()) {
+ processObjectExpression(rightPath);
+ }
+ }
+}
+
+/**
+ * Compile a dynamic style { property: dynamicValue } into a hoisted function pattern.
+ * This creates: const _styles = { property: _v => [compiledStyle, inlineVars] }
+ * And replaces the original with: _styles.property(dynamicValue)
+ */
+function compileDynamicStyle(
+ objPath: NodePath,
+ property: string,
+ valueExpr: t.Expression,
+ state: StateManager,
+): void {
+ // Use a CSS variable for the dynamic value
+ const varName = `--x-${property}`;
+ const rawStyle = { [property]: `var(${varName})` };
+
+ // Create a namespace wrapper for styleXCreateSet
+ const styleInput = { __inline__: rawStyle };
+
+ // eslint-disable-next-line prefer-const
+ let [compiledStyles, injectedStyles] = styleXCreateSet(
+ styleInput,
+ state.options,
+ );
+
+ // Inject dev class names if enabled
+ if (state.isDev && state.opts.enableDevClassNames) {
+ compiledStyles = {
+ ...injectDevClassNames(compiledStyles, null, state),
+ };
+ }
+
+ // Add @property rule for the CSS variable
+ injectedStyles[varName] = {
+ priority: 0,
+ ltr: `@property ${varName} { syntax: "*"; inherits: false;}`,
+ rtl: null,
+ };
+
+ // Register the CSS
+ const listOfStyles = Object.entries(injectedStyles).map(
+ ([key, { priority, ...rest }]) => [key, rest, priority],
+ );
+ state.registerStyles(listOfStyles, objPath);
+
+ // Get the compiled style
+ const compiledStyle = compiledStyles.__inline__;
+ if (compiledStyle == null) {
+ return;
+ }
+
+ // Get the class name for the property
+ const propKey = Object.keys(compiledStyle).find((k) => k !== '$$css');
+ const className = propKey != null ? compiledStyle[propKey] : null;
+ if (className == null || propKey == null) {
+ return;
+ }
+
+ // Create the function pattern:
+ // _v => [{ propKey: _v != null ? className : _v, $$css: true }, { varName: _v != null ? _v : undefined }]
+ const param = t.identifier('_v');
+ const nullCheck = t.binaryExpression('!=', param, t.nullLiteral());
+
+ const compiledObj = t.objectExpression([
+ t.objectProperty(
+ t.stringLiteral(propKey),
+ t.conditionalExpression(nullCheck, t.stringLiteral(className), param),
+ ),
+ t.objectProperty(t.stringLiteral('$$css'), t.booleanLiteral(true)),
+ ]);
+
+ const inlineVarsObj = t.objectExpression([
+ t.objectProperty(
+ t.stringLiteral(varName),
+ t.conditionalExpression(
+ t.binaryExpression('!=', param, t.nullLiteral()),
+ param,
+ t.identifier('undefined'),
+ ),
+ ),
+ ]);
+
+ const fnBody = t.arrayExpression([compiledObj, inlineVarsObj]);
+ const arrowFn = t.arrowFunctionExpression([param], fnBody);
+
+ // Create the hoisted styles object: const _styles = { property: arrowFn }
+ const stylesObj = t.objectExpression([
+ t.objectProperty(t.identifier(property), arrowFn),
+ ]);
+
+ // Hoist the function
+ const hoistedId = hoistExpression(objPath, stylesObj);
+
+ // Replace the original with: _hoistedId.property(valueExpr)
+ const callExpr = t.callExpression(
+ t.memberExpression(hoistedId, t.identifier(property)),
+ [valueExpr],
+ );
+ objPath.replaceWith(callExpr);
+}
diff --git a/packages/@stylexjs/stylex/package.json b/packages/@stylexjs/stylex/package.json
index e8fa7897d..355f81d71 100644
--- a/packages/@stylexjs/stylex/package.json
+++ b/packages/@stylexjs/stylex/package.json
@@ -37,6 +37,7 @@
"test": "cross-env BABEL_ENV=test jest --coverage"
},
"dependencies": {
+ "@stylexjs/atoms": "0.17.5",
"css-mediaquery": "^0.1.2",
"invariant": "^2.2.4",
"styleq": "0.2.1"
diff --git a/packages/@stylexjs/stylex/src/stylex.js b/packages/@stylexjs/stylex/src/stylex.js
index ff5185734..c00bb3cf8 100644
--- a/packages/@stylexjs/stylex/src/stylex.js
+++ b/packages/@stylexjs/stylex/src/stylex.js
@@ -284,3 +284,6 @@ _legacyMerge.when = when;
_legacyMerge.viewTransitionClass = viewTransitionClass;
export const legacyMerge: IStyleX = _legacyMerge;
+
+// Re-export atoms for cleaner imports
+export { default as x } from '@stylexjs/atoms';