Skip to content

Commit 06ada7a

Browse files
committed
docs: add Vue widgets documentation for custom nodes
1 parent 12a92a4 commit 06ada7a

File tree

5 files changed

+386
-6
lines changed

5 files changed

+386
-6
lines changed

custom-nodes/js/javascript_overview.mdx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,13 @@ Comfy can be modified through an extensions mechanism. To add an extension you n
1010
- Place one or more `.js` files into that directory,
1111
- Use `app.registerExtension` to register your extension.
1212

13-
These three steps are below. Once you know how to add an extension, look
14-
through the [hooks](/custom-nodes/js/javascript_hooks) available to get your code called,
13+
These three steps are below. Once you know how to add an extension, look
14+
through the [hooks](/custom-nodes/js/javascript_hooks) available to get your code called,
1515
a description of various [Comfy objects](/custom-nodes/js/javascript_objects_and_hijacking) you might need,
1616
or jump straight to some [example code snippets](/custom-nodes/js/javascript_examples).
1717

18+
For building custom node widgets with Vue components, see [Nodes 2.0 Widgets](/custom-nodes/js/vue_widgets).
19+
1820
### Exporting `WEB_DIRECTORY`
1921

2022
The Comfy web client can be extended by creating a subdirectory in your custom node directory, conventionally called `js`, and

custom-nodes/js/javascript_sidebar_tabs.mdx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,8 @@ app.extensionManager.registerSidebarTab({
131131

132132
For a real-world example of a React application integrated as a sidebar tab, check out the [ComfyUI-Copilot project on GitHub](https://github.com/AIDC-AI/ComfyUI-Copilot).
133133

134+
<Tip>For creating custom node widgets with Vue components, see [Nodes 2.0 Widgets](/custom-nodes/js/vue_widgets).</Tip>
135+
134136
## Dynamic Content Updates
135137

136138
You can update sidebar content in response to graph changes:

custom-nodes/js/vue_widgets.mdx

Lines changed: 373 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,373 @@
1+
---
2+
title: "Nodes 2.0 Widgets"
3+
description: "Create custom node widgets using Vue Single File Components"
4+
---
5+
6+
Nodes 2.0 widgets allow you to create rich, interactive node widgets using Vue 3 Single File Components (SFCs). This is the recommended approach for building custom widgets that need complex UI interactions, state management, or styling.
7+
8+
## Overview
9+
10+
You can create Vue-based widgets using the `getCustomVueWidgets()` hook. ComfyUI exposes Vue globally as `window.Vue`, so your extension uses the same Vue instance as the main app (smaller bundle size).
11+
12+
## Project Structure
13+
14+
```
15+
test_vue_widget_node/
16+
├── __init__.py # Python node definitions
17+
└── web/
18+
├── src/
19+
│ ├── extension.js # Entry point - registers extension
20+
│ ├── styles.css # Tailwind directives
21+
│ └── WidgetStarRating.vue
22+
├── dist/
23+
│ └── extension.js # Built output (loaded by ComfyUI)
24+
├── package.json
25+
├── vite.config.ts
26+
├── tailwind.config.js
27+
└── postcss.config.js
28+
```
29+
30+
## Complete Example
31+
32+
### package.json
33+
34+
```json
35+
{
36+
"name": "test-vue-widget-node",
37+
"version": "1.0.0",
38+
"private": true,
39+
"type": "module",
40+
"scripts": {
41+
"build": "vite build",
42+
"dev": "vite build --watch"
43+
},
44+
"devDependencies": {
45+
"@vitejs/plugin-vue": "^5.0.0",
46+
"autoprefixer": "^10.4.20",
47+
"postcss": "^8.4.49",
48+
"rollup-plugin-external-globals": "^0.13.0",
49+
"tailwindcss": "^3.4.17",
50+
"vite": "^6.0.0",
51+
"vite-plugin-css-injected-by-js": "^3.5.2",
52+
"vue": "^3.5.0"
53+
}
54+
}
55+
```
56+
57+
### vite.config.ts
58+
59+
```typescript
60+
import vue from '@vitejs/plugin-vue'
61+
import externalGlobals from 'rollup-plugin-external-globals'
62+
import { defineConfig } from 'vite'
63+
import cssInjectedByJsPlugin from 'vite-plugin-css-injected-by-js'
64+
65+
export default defineConfig({
66+
plugins: [vue(), cssInjectedByJsPlugin()],
67+
build: {
68+
lib: {
69+
entry: 'src/extension.js',
70+
name: 'TestVueWidgets',
71+
fileName: () => 'extension.js',
72+
formats: ['es']
73+
},
74+
outDir: 'dist',
75+
emptyOutDir: true,
76+
cssCodeSplit: false,
77+
rollupOptions: {
78+
external: ['vue', /^\.\.\/.*\.js$/],
79+
plugins: [externalGlobals({ vue: 'Vue' })]
80+
}
81+
}
82+
})
83+
```
84+
85+
Key configuration points:
86+
87+
| Option | Purpose |
88+
|--------|---------|
89+
| `cssInjectedByJsPlugin()` | Inlines CSS into the JS bundle |
90+
| `external: ['vue', ...]` | Don't bundle Vue, use global |
91+
| `externalGlobals({ vue: 'Vue' })` | Map Vue imports to `window.Vue` |
92+
93+
### extension.js
94+
95+
```javascript
96+
/**
97+
* Test Vue Widget Extension
98+
*
99+
* Demonstrates how to register custom Vue widgets for ComfyUI nodes.
100+
* Widgets are built from .vue SFC files using Vite.
101+
*/
102+
103+
import './styles.css'
104+
import { app } from '../../scripts/app.js'
105+
106+
// Import Vue components
107+
import WidgetStarRating from './WidgetStarRating.vue'
108+
109+
// Register the extension
110+
app.registerExtension({
111+
name: 'TestVueWidgets',
112+
113+
getCustomVueWidgets() {
114+
return {
115+
star_rating: {
116+
component: WidgetStarRating,
117+
aliases: ['STAR_RATING']
118+
}
119+
}
120+
}
121+
})
122+
```
123+
124+
### WidgetStarRating.vue
125+
126+
```vue
127+
<template>
128+
<div class="flex items-center gap-1 py-1">
129+
<span
130+
v-if="widget.label || widget.name"
131+
class="text-xs text-gray-400 min-w-[60px] truncate"
132+
>
133+
{{ widget.label ?? widget.name }}
134+
</span>
135+
<div class="flex gap-0.5">
136+
<button
137+
v-for="star in maxStars"
138+
:key="star"
139+
type="button"
140+
class="border-none bg-transparent p-0 text-lg transition-transform duration-100 hover:scale-110 disabled:cursor-not-allowed disabled:opacity-50"
141+
:class="star <= modelValue ? 'text-yellow-400' : 'text-gray-500'"
142+
:disabled="widget.options?.disabled"
143+
:aria-label="`Rate ${star} out of ${maxStars}`"
144+
@click="setRating(star)"
145+
>
146+
147+
</button>
148+
</div>
149+
<span class="text-xs text-gray-400 ml-1">
150+
{{ modelValue }}/{{ maxStars }}
151+
</span>
152+
</div>
153+
</template>
154+
155+
<script setup>
156+
const { computed } = window.Vue
157+
158+
const props = defineProps({
159+
widget: {
160+
type: Object,
161+
required: true
162+
}
163+
})
164+
165+
const modelValue = defineModel({ default: 0 })
166+
167+
const maxStars = computed(() => props.widget.options?.maxStars ?? 5)
168+
169+
function setRating(value) {
170+
if (props.widget.options?.disabled) return
171+
modelValue.value = modelValue.value === value ? 0 : value
172+
}
173+
</script>
174+
```
175+
176+
### __init__.py
177+
178+
```python
179+
"""
180+
Test Vue Widget Node
181+
182+
A test custom node that demonstrates the Vue widget registration feature.
183+
This node uses a custom STAR_RATING widget type that is rendered by a Vue component.
184+
"""
185+
186+
187+
class TestVueWidgetNode:
188+
"""A test node with a custom Vue-rendered star rating widget."""
189+
190+
@classmethod
191+
def INPUT_TYPES(cls):
192+
return {
193+
"required": {
194+
"rating": ("INT", {
195+
"default": 3,
196+
"min": 0,
197+
"max": 5,
198+
"display": "star_rating", # Custom widget type hint
199+
}),
200+
"text_input": ("STRING", {
201+
"default": "Hello Vue Widgets!",
202+
"multiline": False,
203+
}),
204+
},
205+
}
206+
207+
RETURN_TYPES = ("INT", "STRING")
208+
RETURN_NAMES = ("rating_value", "text_value")
209+
FUNCTION = "process"
210+
CATEGORY = "Testing/Vue Widgets"
211+
DESCRIPTION = "Test node for Vue widget registration feature"
212+
213+
def process(self, rating: int, text_input: str):
214+
print(f"[TestVueWidgetNode] Rating: {rating}, Text: {text_input}")
215+
return (rating, text_input)
216+
217+
218+
NODE_CLASS_MAPPINGS = {
219+
"TestVueWidgetNode": TestVueWidgetNode,
220+
}
221+
222+
NODE_DISPLAY_NAME_MAPPINGS = {
223+
"TestVueWidgetNode": "Test Vue Widget (Star Rating)",
224+
}
225+
226+
WEB_DIRECTORY = "./web/dist"
227+
__all__ = ['NODE_CLASS_MAPPINGS', 'WEB_DIRECTORY']
228+
```
229+
230+
## Using Tailwind CSS
231+
232+
### tailwind.config.js
233+
234+
```javascript
235+
/** @type {import('tailwindcss').Config} */
236+
export default {
237+
content: ['./src/**/*.{vue,js,ts}'],
238+
corePlugins: {
239+
preflight: false // Disable base reset to avoid affecting other parts of the app
240+
},
241+
theme: {
242+
extend: {}
243+
},
244+
plugins: []
245+
}
246+
```
247+
248+
<Warning>Always set `preflight: false` to prevent Tailwind's CSS reset from affecting ComfyUI's styles.</Warning>
249+
250+
### postcss.config.js
251+
252+
```javascript
253+
export default {
254+
plugins: {
255+
tailwindcss: {},
256+
autoprefixer: {}
257+
}
258+
}
259+
```
260+
261+
### styles.css
262+
263+
```css
264+
@tailwind base;
265+
@tailwind components;
266+
@tailwind utilities;
267+
```
268+
269+
### Handling Positioning
270+
271+
Without Tailwind's preflight, some utility classes like `absolute`, `translate-x-1/2` may not work as expected. Use inline styles for critical positioning:
272+
273+
```vue
274+
<div
275+
:style="{
276+
position: 'absolute',
277+
top: '50%',
278+
left: '50%',
279+
transform: 'translate(-50%, -50%)'
280+
}"
281+
>
282+
Centered content
283+
</div>
284+
```
285+
286+
## Widget Component API
287+
288+
Your Vue component receives these props:
289+
290+
| Prop | Type | Description |
291+
|------|------|-------------|
292+
| `widget` | `Object` | Widget configuration object |
293+
| `widget.name` | `string` | Widget name from Python input |
294+
| `widget.label` | `string` | Display label |
295+
| `widget.options` | `Object` | Options from Python (`min`, `max`, `step`, etc.) |
296+
| `widget.options.disabled` | `boolean` | Whether the widget is disabled |
297+
298+
Use `defineModel()` for two-way value binding:
299+
300+
```vue
301+
<script setup>
302+
const modelValue = defineModel({ default: 0 })
303+
</script>
304+
```
305+
306+
## Build and Test
307+
308+
```bash
309+
cd web
310+
npm install
311+
npm run build
312+
```
313+
314+
Restart ComfyUI to load your extension.
315+
316+
## Development Workflow
317+
318+
Run the watcher during development:
319+
320+
```bash
321+
npm run dev
322+
```
323+
324+
This rebuilds `dist/extension.js` on every file change. Refresh ComfyUI to see updates.
325+
326+
## Common Pitfalls
327+
328+
### Failed to resolve module specifier "vue"
329+
330+
The browser can't resolve bare `"vue"` imports.
331+
332+
**Solution:** Use `rollup-plugin-external-globals` to map Vue imports to `window.Vue`:
333+
334+
```typescript
335+
import externalGlobals from 'rollup-plugin-external-globals'
336+
337+
rollupOptions: {
338+
external: ['vue', /^\.\.\/.*\.js$/],
339+
plugins: [externalGlobals({ vue: 'Vue' })]
340+
}
341+
```
342+
343+
### CSS Not Loading
344+
345+
ComfyUI only loads JS files. CSS must be inlined into the bundle.
346+
347+
**Solution:** Use `vite-plugin-css-injected-by-js`.
348+
349+
### Styles Affecting Other UI
350+
351+
Tailwind's preflight resets global styles, breaking ComfyUI.
352+
353+
**Solution:** Set `preflight: false` in `tailwind.config.js`.
354+
355+
### Accessing ComfyUI's App
356+
357+
Import from the relative path to `scripts/app.js`:
358+
359+
```javascript
360+
import { app } from '../../scripts/app.js'
361+
```
362+
363+
Configure Vite to preserve this import:
364+
365+
```typescript
366+
rollupOptions: {
367+
external: [/^\.\.\/.*\.js$/]
368+
}
369+
```
370+
371+
## Examples
372+
373+
- [ComfyUI_frontend_vue_basic](https://github.com/jtydhr88/ComfyUI_frontend_vue_basic) - Full Vue extension with PrimeVue, i18n, drawing board widget

0 commit comments

Comments
 (0)