diff --git a/.changepacks/changepack_log_stylex_attrs_style.json b/.changepacks/changepack_log_stylex_attrs_style.json
new file mode 100644
index 000000000..7ccca57d3
--- /dev/null
+++ b/.changepacks/changepack_log_stylex_attrs_style.json
@@ -0,0 +1,8 @@
+{
+ "changes": {
+ "packages/react/package.json": "Patch",
+ "bindings/devup-ui-wasm/package.json": "Patch"
+ },
+ "note": "stylex.attrs() gives the dynamic CSS variables it sets as `style` attribute text (`\"--a:\" + value`), as StyleX does for spreading onto non-React targets, instead of a style object; stylex.props() keeps the object React takes. The attrs() return type declares `style` as a string",
+ "date": "2026-10-01T00:00:00.000Z"
+}
diff --git a/libs/extractor/src/lib.rs b/libs/extractor/src/lib.rs
index a5853d5b5..8d63e0ffe 100644
--- a/libs/extractor/src/lib.rs
+++ b/libs/extractor/src/lib.rs
@@ -20814,6 +20814,25 @@ const e =
;"
)));
}
+ // `attrs()` gives the `style` attribute as text, as StyleX does, while
+ // `props()` keeps the object React takes
+ #[test]
+ #[serial]
+ fn test_stylex_attrs_style_is_text() {
+ let code = extract_tsx(
+ r"import * as stylex from '@stylexjs/stylex';
+const s = stylex.create({ box: (w, h) => ({ width: w, height: h }) });
+export const a = stylex.attrs(s.box(w, h));
+export const p = stylex.props(s.box(w, h));",
+ )
+ .code;
+ assert!(
+ code.contains("style: \"--a:\" + ((v) => typeof v === \"number\" ? v + \"px\" : v)(w) + \";--b:\" + ((v) => typeof v === \"number\" ? v + \"px\" : v)(h)"),
+ "{code}"
+ );
+ assert!(code.contains("style: {\n\t\t\"--a\":"), "{code}");
+ }
+
#[test]
#[serial]
fn test_emotion_global_with_spread_attribute() {
diff --git a/libs/extractor/src/visit.rs b/libs/extractor/src/visit.rs
index 93846123e..39e65e43c 100644
--- a/libs/extractor/src/visit.rs
+++ b/libs/extractor/src/visit.rs
@@ -637,6 +637,47 @@ impl<'a> DevupVisitor<'a> {
self.stylex_function(callee).as_ref() == Some(function)
}
+ /// `"--a:" + a + ";--b:" + b`: the variables `props` sets, as the text of a
+ /// `style` attribute
+ fn style_text(&self, props: Vec>) -> Expression<'a> {
+ let mut text: Option> = None;
+ let props = props.into_iter().filter_map(|prop| {
+ if let ObjectPropertyKind::ObjectProperty(prop) = prop {
+ Some(prop.unbox())
+ } else {
+ None
+ }
+ });
+ for (index, prop) in props.enumerate() {
+ let name = get_string_by_property_key(&prop.key).unwrap_or_default();
+ let prefix = format!("{}{name}:", if index == 0 { "" } else { ";" });
+ let head = Expression::new_string_literal(
+ SPAN,
+ Str::from_in(prefix.as_str(), self.ast.allocator()),
+ None,
+ &self.ast,
+ );
+ let head = match text {
+ Some(text) => Expression::new_binary_expression(
+ SPAN,
+ text,
+ BinaryOperator::Addition,
+ head,
+ &self.ast,
+ ),
+ None => head,
+ };
+ text = Some(Expression::new_binary_expression(
+ SPAN,
+ head,
+ BinaryOperator::Addition,
+ Expression::new_parenthesized_expression(SPAN, prop.value, &self.ast),
+ &self.ast,
+ ));
+ }
+ text.unwrap_or_else(|| Expression::new_string_literal(SPAN, "", None, &self.ast))
+ }
+
/// Resolve `stylex.props()` arguments to className expressions and style properties.
/// Returns (`class_exprs`, `style_props`) where `style_props` are CSS variable assignments
/// from dynamic namespace calls like `styles.bar(h)`.
@@ -1565,11 +1606,16 @@ impl<'a> VisitMut<'a> for DevupVisitor<'a> {
// Add style property for dynamic CSS variables
if !style_props.is_empty() {
- let style_obj = Expression::new_object_expression(
- SPAN,
- oxc_allocator::Vec::from_iter_in(style_props, &self.ast),
- &self.ast,
- );
+ // `attrs()` gives the `style` attribute as text, `props()` an object
+ let style_obj = if class_attribute == "class" {
+ self.style_text(style_props)
+ } else {
+ Expression::new_object_expression(
+ SPAN,
+ oxc_allocator::Vec::from_iter_in(style_props, &self.ast),
+ &self.ast,
+ )
+ };
props.push(ObjectPropertyKind::new_object_property(
SPAN,
PropertyKind::Init,
diff --git a/packages/react/src/utils/stylex.ts b/packages/react/src/utils/stylex.ts
index e46905f6c..e0f051acd 100644
--- a/packages/react/src/utils/stylex.ts
+++ b/packages/react/src/utils/stylex.ts
@@ -32,7 +32,7 @@ export function props(
export function attrs(
..._styles: ReadonlyArray
-): { class?: string; style?: Record } {
+): { class?: string; style?: string } {
throw new Error('Cannot run on the runtime')
}