diff --git a/demo/vite-project/src/components/tanstack-form-test.tsx b/demo/vite-project/src/components/tanstack-form-test.tsx
new file mode 100644
index 000000000..247060b8b
--- /dev/null
+++ b/demo/vite-project/src/components/tanstack-form-test.tsx
@@ -0,0 +1,123 @@
+import React from 'react';
+
+// Simulating Tanstack Forms pattern
+function useForm() {
+ return {
+ AppForm: ({ children }: { children: React.ReactNode }) => (
+
+ ),
+ CancelButton: ({ children, onClick }: { children: React.ReactNode; onClick?: () => void }) => (
+
+ ),
+ SubmitButton: ({ children }: { children: React.ReactNode }) => (
+
+ ),
+ };
+}
+
+// Simulating uppercase workaround
+function useFormUppercase() {
+ return {
+ AppForm: ({ children }: { children: React.ReactNode }) => (
+
+ ),
+ CancelButton: ({ children, onClick }: { children: React.ReactNode; onClick?: () => void }) => (
+
+ ),
+ SubmitButton: ({ children }: { children: React.ReactNode }) => (
+
+ ),
+ };
+}
+
+export default function TanstackFormTest() {
+ const form = useForm();
+ const FormContent = useFormUppercase();
+
+ const handleCancel = () => {
+ alert('Cancel clicked!');
+ };
+
+ return (
+
+
Tanstack Forms + Lingo.dev Compiler Issue #1165
+
+
+
✅ Previously broken: Lowercase variable name (form) - now fixed
+
Using: const form = useForm()
+
Expected: Blue border, styled buttons with click functionality
+
+
+
Cancel
+ Submit
+
+
+
+
+
+
Working: Uppercase variable name (FormContent)
+
Using: const FormContent = useFormUppercase()
+
Expected: Green border, styled buttons with click functionality
+
+
+ Cancel
+ Submit
+
+
+
+
+ );
+}
+
diff --git a/packages/compiler/src/jsx-scope-inject.ts b/packages/compiler/src/jsx-scope-inject.ts
index d1003a68a..a69c8c689 100644
--- a/packages/compiler/src/jsx-scope-inject.ts
+++ b/packages/compiler/src/jsx-scope-inject.ts
@@ -14,6 +14,25 @@ import { getJsxExpressions } from "./utils/jsx-expressions";
import { collectJsxScopes, getJsxScopeAttribute } from "./utils/jsx-scope";
import { setJsxAttributeValue } from "./utils/jsx-attribute";
+/**
+ * Creates a proper AST node from a string that may contain dots (member expressions).
+ * For example, "form.Button" becomes a member expression AST, while "Button" becomes an identifier.
+ * @param str - The string to convert (e.g., "form.Button" or "Button")
+ * @returns A Babel AST Expression node
+ */
+function createMemberExpressionFromString(str: string): t.Expression {
+ const parts = str.split('.');
+ if (parts.length === 1) {
+ return t.identifier(parts[0]);
+ }
+
+ let expr: t.Expression = t.identifier(parts[0]);
+ for (let i = 1; i < parts.length; i++) {
+ expr = t.memberExpression(expr, t.identifier(parts[i]));
+ }
+ return expr;
+}
+
export const lingoJsxScopeInjectMutation = createCodeMutation((payload) => {
const mode = getModuleExecutionMode(payload.ast, payload.params.rsc);
const jsxScopes = collectJsxScopes(payload.ast);
@@ -55,8 +74,13 @@ export const lingoJsxScopeInjectMutation = createCodeMutation((payload) => {
} as any;
// Add $as prop
- const as = /^[A-Z]/.test(originalJsxElementName)
- ? t.identifier(originalJsxElementName)
+ // Check if it's a member expression (contains dot) or starts with uppercase.
+ // Member expressions (e.g., form.Button) and uppercase names (e.g., Button)
+ // should be treated as component references, not HTML element strings.
+ const isMemberExpression = originalJsxElementName.includes(".");
+ const isComponent = /^[A-Z]/.test(originalJsxElementName);
+ const as = isMemberExpression || isComponent
+ ? createMemberExpressionFromString(originalJsxElementName)
: originalJsxElementName;
setJsxAttributeValue(newNodePath, "$as", as);