cabbage3docs

Cabbage 3 includes a variety of standard plugin controls such as sliders and buttons. However, it also provides straightforward options for creating your own custom elements. There are two primary methods for building custom UIs:

  1. Custom Widget Classes

Create a new widget class and add it to Cabbage. This approach allows you to develop custom widgets while retaining the convenience of the editing tools available in the Cabbage VS Code extension. To achieve this, use the command palette to Create New Custom Widget. If you haven’t already created a custom widget folder, you’ll need to do so first. When this folder is created, the extension will copy the required files into it.

All custom widget classes should be placed in this folder’s widgets sub-folder, i.e, CustomWidgetFolder/cabbage/widgets, so the extension can locate them and load them via the property panel. The name of the file you create will also serve as the class name.

By convention, Cabbage classes follow UpperCamelCase, while widget types use lowerCamelCase. The backend handles all value normalization needed by the host DAW, so frontends send and receive values in their full ranges (e.g., 20-20000 Hz for filter frequency, not normalized 0.0-1.0 values).

For your widget to be recognized by the Cabbage VS Code extension—especially by the property panel—you must:

Both methods need to be implemented to ensure compatibility with different environments. The simplest pattern is to put all the event handler code into the addEventListener() method, and then call it from the addVsCodeEventListeners() method.

this.props = CabbageUtils.createReactiveProps(this, this.props, {
    onPropertyChange: (change) => {
        // Called when watched properties change
        // change.key - the property name
        // change.value - new value
        // change.oldValue - previous value
        // change.path - dot-separated path to the property
    },
    watchKeys: null,        // null = watch all, or array of strings/RegExp (e.g., ['value', 'bounds*'])
    mode: 'change',         // 'change' (default) = only notify when value differs, 'set' = notify on every set
    lazyPath: true          // true (default) = compute path only when notifying (more efficient)
});

The opts parameter is optional - you can call CabbageUtils.createReactiveProps(this, this.props) without options for basic reactive behavior.

Performance Note: Don’t use onPropertyChange to listen for value updates from Csound resulting from calls to cabbageSetValue. The WidgetManager automatically calls updateCanvas() for canvas-based widgets when channel values change, bypassing the reactive props system for efficiency. Value updates are sent as lightweight messages without full widget JSON, allowing high-frequency updates (e.g., for k-rate parameter changes) without overhead. Use reactive props for UI-driven property changes (like visible, bounds, or custom properties), not for high frequency value streams.

Sending Data to the Backend

To send data from your widget to the Cabbage backend, use Cabbage.sendControlData():

Cabbage.sendControlData({ channel: "myChannel", value: 42, gesture: "complete" }, this.vscode);

Parameters:

Behavior:

Value Ranges: Send values in their natural/meaningful range (e.g., 20-20000 Hz for filter frequency). The backend automatically handles all normalization needed by the host DAW.

Gesture Types: Use appropriate gestures for DAW automation:

For most use cases, sendControlData() is the recommended API as it automatically handles routing. However, you can also call Cabbage.sendChannelData(channel, data, vscode) directly for non-automatable data:

Cabbage.sendChannelData("myStringChannel", "hello", this.vscode);
Cabbage.sendChannelData("myNumberChannel", 3.14, this.vscode);

Value Ranges: Send values in their natural/meaningful range. The backend handles all normalization needed by the host DAW.

Channel Communication

The channels array defines the channels used to send value updates from the front end to the back end. If no this.props.id is defined, the first channel.id in the channels array will be used as the primary DOM identifier.

For multi-channel widgets (like an EQ controller with separate frequency and gain channels for each band), the WidgetManager automatically routes incoming parameter updates to the correct channel based on the channel ID. Each channel in the array should have:

CabbageUtils Helper Functions

The CabbageUtils class provides several helper functions for working with widgets:

CabbageUtils.getWidgetDivId(props)

const divId = CabbageUtils.getWidgetDivId(this.props);

CabbageUtils.getWidgetDiv(channelOrProps)

const widgetDiv = CabbageUtils.getWidgetDiv(this.props);
// or
const widgetDiv = CabbageUtils.getWidgetDiv("myWidgetId");

CabbageUtils.getChannelId(props, index)

const firstChannelId = CabbageUtils.getChannelId(this.props, 0);
const secondChannelId = CabbageUtils.getChannelId(this.props, 1);
  1. Entirely new web-based interfaces

It’s also possible to design a completely custom web-based interface using any framework you like. To enable communication with the Csound/Cabbage plugin, you simply need to include the cabbage.js file, which provides the core functions required to send data from the web UI into Csound. While this approach does not provide access to Cabbage’s built-in UI editing tools, it offers maximum flexibility for building interfaces tailored to your needs.

If you want to create an entirely new frontend, with Svelte, React or even vanilla JS, can use the Command Palette to generate a new plugin project. This will create a basic project layout with an HTML file, a CSS file, and a JavaScript file. From there, you can use the Cabbage JS API to communicate with Csound and build your interface however you wish.

To communicate with Csound, you will need to implement event handlers for sending and receiving data. The following example demonstrates a complete setup that communicates to two parameters, with ids slider1 and slider2:

<script type="module">
    /* Cabbage JS API integration */
    import { Cabbage } from './cabbage/cabbage.js';
    /* Notify Cabbage that the UI is ready to load */
    Cabbage.isReadyToLoad();

    // Make handleValueChange available globally
    window.handleValueChange = (newValue, sliderId) => {
        console.log(`Slider ${sliderId} changed to:`, newValue);
        // Send the value directly - backend automatically determines if channel is automatable
        Cabbage.sendControlData({ channel: sliderId, value: parseFloat(newValue), gesture: "complete" }, null);
    };

    const handleMessage = async (event) => {
        console.log("Message received:", event.data);
        let obj = event.data;

        let slider;
        if (obj.command === "parameterChange") {
            // For parameterChange messages, find slider by paramIdx
            slider = obj.paramIdx === 0 ? document.getElementById('slider1') : document.getElementById('slider2');
        } else {
            // For other messages, find slider by id
            slider = document.getElementById(obj.id);
        }

        if (slider) {
            switch (obj.command) {
                case "parameterChange":
                    console.log(`Parameter change for ${obj.paramIdx}:`, obj);
                    slider.value = obj.value;
                    break;
                case "widgetUpdate":
                    if (obj.value !== undefined) {
                        console.log(`Updating ${obj.id} to value:`, obj.value);
                        slider.value = obj.value;
                    }
                    else if (obj.widgetJson !== undefined) {
                        let widgetObj = JSON.parse(obj.widgetJson);
                        let bounds = widgetObj.bounds;
                        if (bounds) {
                            slider.style.position = 'absolute';
                            slider.style.top = bounds.top + 'px';
                        }
                        // Set value if the UI has just been reopened
                        if (widgetObj.value !== undefined) {
                            slider.value = widgetObj.value;
                        }
                    }
                    break;
                default:
                    break;
            }
        }
    };

    // Add event listener
    window.addEventListener("message", handleMessage);
</script>

The script starts by sending a cabbageIsReadyToLoad message via the Cabbage.isReadyToLoad() function. This is essential because it informs Cabbage that the web interface is fully loaded and ready to start exchanging data. Without this step, the plugin might miss updates or fail to synchronise with the custom UI.

User interactions, like moving a slider or changing a control, are captured by the global function handleValueChange. This function sends the new value directly to Cabbage using Cabbage.sendControlData(). The backend automatically determines whether the channel is automatable and routes the data appropriately - either to the DAW parameter system for automation, or directly to Csound for non-automatable channels. The vscode parameter is null when working outside the VS Code environment.

The script also sets up a handleMessage listener to capture messages from Csound or the DAW. Messages can be either parameter values sent from the host, or through calls to the cabbageSetValue/cabbageSet opcodes. Host parameter change messages are formatted like this:

{
    "command": "parameterChange",
    "paramIdx": number,
    "value": number
}

while value updates from Csound, though calls to cabbageSetValue, are formatted like this:

{
    "command": "widgetUpdate",
    "id": string,
    "value": value
}

Messages can also contain Json data, which can modify widget properties such as visibility or styling. These arrive from calls to the cabbageSet opcodes, and are structured like this:

{
    "command": "widgetUpdate",
    "id": string,
    "widgetJson": string
}

You must add dummy parameters to the Cabbage section so that the software can set up the necessary channels and plugin parameters*.