diff --git a/packages/ui/build/common-config.ts b/packages/ui/build/common-config.ts
index 16707489e9..1a1b805cfa 100644
--- a/packages/ui/build/common-config.ts
+++ b/packages/ui/build/common-config.ts
@@ -1,3 +1,4 @@
+import { componentVBindFix } from './plugins/component-v-bind-fix';
import { readFileSync, lstatSync, readdirSync } from 'fs'
import vue from '@vitejs/plugin-vue'
import { resolve as resolver } from 'path'
@@ -102,6 +103,7 @@ export default function createViteConfig (format: BuildFormat) {
isEsm && config.plugins.push(chunkSplitPlugin({ strategy: 'unbundle' }))
isEsm && !isNode && config.plugins.push(appendComponentCss())
isEsm && config.plugins.push(removeSideEffectedChunks())
+ isEsm && config.plugins.push(componentVBindFix())
config.build.rollupOptions = isNode ? { ...external, ...rollupMjsBuildOptions } : external
diff --git a/packages/ui/build/plugins/component-v-bind-fix.spec.ts b/packages/ui/build/plugins/component-v-bind-fix.spec.ts
new file mode 100644
index 0000000000..f23471f530
--- /dev/null
+++ b/packages/ui/build/plugins/component-v-bind-fix.spec.ts
@@ -0,0 +1,178 @@
+import { describe, test, expect } from 'vitest'
+import { transformVueComponent } from './component-v-bind-fix'
+
+describe('component-v-bind-fix', () => {
+ describe('transformVueComponent', () => {
+ // eslint-disable-next-line no-template-curly-in-string
+ const expectedStyleString = '--va-0-color: ${String(color)};--va-1-background: ${String(background)}'
+
+ const expectedObjectStyleString = (styleContent: string) => `typeof ${styleContent} === 'object' ? (Array.isArray(${styleContent}) ? [...${styleContent}, \`${expectedStyleString}\`] : { ...${styleContent}, '--va-0-color': String(color),'--va-1-background': String(background) }) : ${styleContent} + \`;${expectedStyleString}\``
+
+ const componentCode = (attrs = '', nestedAttrs = '') => `
+
+
+
+
+
+
+
+`
+
+ const expectedComponentCode = (attrs = '', nestedAttrs = '') => `
+
+
+
+
+
+
+
+`
+
+ test('expectedObjectStyleString', () => {
+ expect(expectedObjectStyleString('style')).toBe(`typeof style === 'object' ? (Array.isArray(style) ? [...style, \`${expectedStyleString}\`] : { ...style, '--va-0-color': String(color),'--va-1-background': String(background) }) : style + \`;${expectedStyleString}\``)
+ })
+
+ test('replace v-bind() with var(--va-index-name)', () => {
+ const code = transformVueComponent(componentCode())
+
+ expect(code).toContain('var(--va-0-color)')
+ expect(code).toContain('var(--va-1-background)')
+
+ expect(code).not.toContain('v-bind(color)')
+ expect(code).not.toContain('v-bind(background)')
+ })
+
+ test("if root node doesn't have a style attr, we should add a style attr with css variables ", () => {
+ const code = transformVueComponent(componentCode())
+
+ expect(code).toBe(expectedComponentCode(
+ `:style="\`${expectedStyleString}\`"`,
+ ))
+ })
+
+ test('if root node have a style attr we add to its value css variables', () => {
+ const code = transformVueComponent(componentCode(
+ 'style="color: red"',
+ ))
+
+ expect(code).toBe(expectedComponentCode(
+ `:style="\`color: red;${expectedStyleString}\`"`,
+ ))
+ })
+
+ test('if root node have a style object vbind, but no style tag we add style attr with css variables', () => {
+ const code = transformVueComponent(componentCode(
+ ':style="{ background: yellow }"',
+ ))
+
+ const obj = "{ ...{ background: yellow }, '--va-0-color': color,'--va-1-background': background }"
+ // eslint-disable-next-line no-template-curly-in-string
+ const str = '{ background: yellow } + `;--va-0-color: ${color};--va-1-background: ${background}`'
+
+ expect(code).toBe(expectedComponentCode(
+ `:style="${expectedObjectStyleString('{ background: yellow }')}"`,
+ ))
+ })
+
+ // TODO: Maybe better just use style string
+ test('if root node have both style attr and vbind we add to style attr css variables', () => {
+ const code = transformVueComponent(componentCode(
+ 'style="color: blue" :style="{ background: yellow }"',
+ ))
+
+ expect(code).toBe(expectedComponentCode(
+ `style="color: blue" :style="${expectedObjectStyleString('{ background: yellow }')}"`,
+ ))
+ })
+
+ test('if root node have vbind and nested element has style attr, we still add style attr on root node', () => {
+ const code = transformVueComponent(componentCode(
+ ':style="{ background: yellow }"',
+ 'style="color: blue"',
+ ))
+
+ expect(code).toBe(expectedComponentCode(
+ `:style="${expectedObjectStyleString('{ background: yellow }')}"`,
+ 'style="color: blue"',
+ ))
+ })
+
+ test('if root node have vbind and nested element has style vbind, we add css variables only to root node', () => {
+ const code = transformVueComponent(componentCode(
+ ':style="{ background: yellow }"',
+ ':style="{ color: blue }"',
+ ))
+
+ expect(code).toBe(expectedComponentCode(
+ `:style="${expectedObjectStyleString('{ background: yellow }')}"`,
+ ':style="{ color: blue }"',
+ ))
+ })
+
+ test('if root node doesn\'t have any style we add it as string and nested elements stay the same', () => {
+ const code = transformVueComponent(componentCode('', ':style="{ color: blue }"'))
+
+ expect(code).toBe(expectedComponentCode(`:style="\`${expectedStyleString}\`"`, ':style="{ color: blue }"'))
+ })
+
+ test('if root node has a lot of attrs we still add style', () => {
+ const code = transformVueComponent(componentCode(`
+ role="button"
+ id="submit"
+ class="btn btn-primary"
+ :class="{ active: isActive }"
+ `.trim()))
+
+ expect(code).toBe(expectedComponentCode(`
+ role="button"
+ id="submit"
+ class="btn btn-primary"
+ :class="{ active: isActive }" :style="\`${expectedStyleString}\`"
+ `.trim()))
+ })
+
+ test('if root node has a lot of attrs', () => {
+ const code = transformVueComponent(componentCode(`
+ role="button"
+ id="submit"
+ class="btn btn-primary"
+ style="cursor: pointer"
+ :class="{ active: isActive }"
+ `.trim()))
+
+ expect(code).toBe(expectedComponentCode(`
+ role="button"
+ id="submit"
+ class="btn btn-primary"
+ :style="\`cursor: pointer;${expectedStyleString}\`"
+ :class="{ active: isActive }"
+ `.trim()))
+ })
+ })
+})
diff --git a/packages/ui/build/plugins/component-v-bind-fix.ts b/packages/ui/build/plugins/component-v-bind-fix.ts
new file mode 100644
index 0000000000..6611e31446
--- /dev/null
+++ b/packages/ui/build/plugins/component-v-bind-fix.ts
@@ -0,0 +1,129 @@
+import { Plugin } from 'vite'
+import kebabCase from 'lodash/kebabCase'
+
+/**
+ * Parse css and extract all variable names used in `v-bind`
+ *
+ * @example
+ *
+ * ```css
+ * .va-button {
+ * color: v-bind(colorComputed);
+ * background-color: v-bind(getBg());
+ * }
+ * ```
+ *
+ * Returns`['colorComputed', 'getBg()']`
+ */
+const parseCssVBindCode = (style: string) => {
+ return style
+ .match(/v-bind\((.*)\)/g)
+ ?.map((line) => line.match(/v-bind\(([^)]*)\)/)![1]) ?? []
+}
+
+/**
+ * @example
+ *
+ * ```html
+ *
+ *
+ * Hello
+ *
+ *
+ * ```
+ * Returns ``
+ */
+const getRootNodeOpenTagCode = (code: string) => {
+ const template = code.match(/]*>([\s\S]*)<\/template>/)?.[1]
+ const rootNode = template?.match(/<[^>]*>/)?.[0]
+ return rootNode
+}
+
+const renderCssVariablesAsStringCode = (vBinds: string[]) => {
+ return vBinds.map((vBind, index) => {
+ return `--va-${index}-${kebabCase(vBind)}: \${String(${vBind})}`
+ }, '').join(';')
+}
+
+const renderCssVariablesAsObjectPropertiesCode = (vBinds: string[]) => {
+ return vBinds.map((vBind, index) => {
+ return `'--va-${index}-${kebabCase(vBind)}': String(${vBind})`
+ }, '').join(',')
+}
+
+const renderObjectGuardCode = (existingContent: string, binds: string[]) => {
+ const renderedAsString = renderCssVariablesAsStringCode(binds)
+ const renderedAsObjectProperties = renderCssVariablesAsObjectPropertiesCode(binds)
+
+ // Merge existing style with rendered css variables
+ const arrayStyle = `[...${existingContent}, \`${renderedAsString}\`]`
+ const objectStyle = `{ ...${existingContent}, ${renderedAsObjectProperties} }`
+ const stringStyle = `${existingContent} + \`;${renderedAsString}\``
+
+ // Handle if style is an object, array or string
+ return `typeof ${existingContent} === 'object' ? (Array.isArray(${existingContent}) ? ${arrayStyle} : ${objectStyle}) : ${stringStyle}`
+}
+
+const addStyleToRootNode = (rootNode: string, vBinds: string[]) => {
+ const [vBindCode, vBindContent] = rootNode?.match(/:style="([^"]*)"/) || []
+ const [attrCode, attrContent] = rootNode?.match(/[^:]style="([^"]*)"/) || []
+ const cssVariablesString = renderCssVariablesAsStringCode(vBinds)
+
+ // If style attr already exists add css variables to it
+ if (vBindContent) {
+ // If :style already exists add a object guard here, because we're not sure if it is string, object or array.
+ return rootNode.replace(/:style="([^"]*)"/, `:style="${renderObjectGuardCode(vBindContent, vBinds)}"`)
+ }
+
+ if (attrContent) {
+ // If style already exists as string attribute, append css variables string to it
+ return rootNode.replace(/style="([^"]*)"/, `:style="\`$1;${cssVariablesString}\`"`)
+ }
+
+ // Add :style to root node, because it doesn't exist yet
+ // Replace `/>` or `>` with :style="..."/> or :style="..."> respectively
+ return rootNode.replace(/(\/?>)$/, ` :style="\`${cssVariablesString}\`"$1`)
+}
+
+/** Replace each v-bind() with var(--va-index-name) */
+const replaceVueVBindWithCssVariables = (code: string, vBinds: string[]) => {
+ vBinds.forEach((vBind, index) => {
+ try {
+ code = code.replace(new RegExp(`v-bind\\(${vBind}\\)`, 'gm'), `var(--va-${index}-${kebabCase(vBind)})`)
+ } catch (e) {
+ console.log(vBind)
+ throw e
+ }
+ })
+
+ return code
+}
+
+export const transformVueComponent = (code: string) => {
+ const style = code.match(/