with one click
juce-webview-windows
// Quick-start guide for building JUCE 8 audio plugins with WebView2 UIs on Windows. Covers essential setup, critical member ordering, and step-by-step implementation workflow.
// Quick-start guide for building JUCE 8 audio plugins with WebView2 UIs on Windows. Covers essential setup, critical member ordering, and step-by-step implementation workflow.
Quick-start guide for building JUCE 8 audio plugins with WebView2 UIs on Windows. Covers essential setup, critical member ordering, and step-by-step implementation workflow.
Quick-start guide for building JUCE 8 audio plugins with WebView2 UIs on Windows. Covers essential setup, critical member ordering, and step-by-step implementation workflow.
Create the Visual Interface for audio plugins. Use when user mentions UI design, mockup, WebView interface, or requests 'design UI for [plugin]'.
Autonomous Debugging Instructions for Visual Studio Code: for [plugin].
| name | juce-webview-windows |
| description | Quick-start guide for building JUCE 8 audio plugins with WebView2 UIs on Windows. Covers essential setup, critical member ordering, and step-by-step implementation workflow. |
Platform: Windows 11 | JUCE 8 | WebView2 | CMake
Build audio plugin UIs using modern web technologies (HTML/CSS/JavaScript) instead of C++ JUCE components.
Benefits:
Trade-offs:
ā ļø #1 CAUSE OF WEBVIEW PLUGIN CRASHES - MUST FOLLOW
C++ destroys members in REVERSE order of declaration. WebView references relays, so relays must be declared FIRST to be destroyed LAST.
private:
YourAudioProcessor& audioProcessor;
// āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
// CRITICAL: Destruction Order = Reverse of Declaration
// Order: Relays ā WebView ā Attachments
// āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
// 1. RELAYS FIRST (destroyed last)
juce::WebSliderRelay gainRelay { "GAIN" };
juce::WebSliderRelay frequencyRelay { "FREQUENCY" };
// 2. WEBVIEW SECOND (destroyed middle)
std::unique_ptr<juce::WebBrowserComponent> webView;
// 3. ATTACHMENTS LAST (destroyed first)
std::unique_ptr<juce::WebSliderParameterAttachment> gainAttachment;
std::unique_ptr<juce::WebSliderParameterAttachment> frequencyAttachment;
Wrong Order ā DAW Crash on Unload
See: .claude/troubleshooting/resolutions/webview-member-order-crash.md
plugins/YourPlugin/
āāā Source/
āāā ui/
āāā public/
āāā index.html
āāā js/
āāā index.js
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>My Plugin</title>
<script type="module" src="js/index.js"></script>
<style>
body {
margin: 0;
padding: 20px;
background: #1a1a2e;
color: #e0e0e0;
font-family: system-ui, sans-serif;
}
input[type="range"] { width: 100%; }
</style>
</head>
<body>
<h1>My Plugin</h1>
<div>
<label for="gainSlider">Gain</label>
<input type="range" id="gainSlider" min="0" max="1" step="0.01" value="0.5">
<span id="gainValue">0.5</span>
</div>
</body>
</html>
import * as Juce from "./juce/index.js";
document.addEventListener("DOMContentLoaded", () => {
// Get parameter state from C++
const gainState = Juce.getSliderState("GAIN");
const gainSlider = document.getElementById("gainSlider");
const gainValue = document.getElementById("gainValue");
// User interaction ā Update C++
gainSlider.addEventListener("mousedown", () => gainState.sliderDragStarted());
gainSlider.addEventListener("mouseup", () => gainState.sliderDragEnded());
gainSlider.addEventListener("input", () => {
gainState.setNormalisedValue(gainSlider.value);
gainValue.textContent = gainSlider.value;
});
// C++ automation ā Update UI
gainState.valueChangedEvent.addListener(() => {
const value = gainState.getNormalisedValue();
gainSlider.value = value;
gainValue.textContent = value.toFixed(2);
});
});
# Embed web files into binary
juce_add_binary_data(YourPlugin_WebUI
SOURCES
Source/ui/public/index.html
Source/ui/public/js/index.js
Source/ui/public/js/juce/index.js
Source/ui/public/js/juce/check_native_interop.js
)
# Plugin definition
juce_add_plugin(YourPlugin
FORMATS VST3 Standalone
PRODUCT_NAME "Your Plugin"
NEEDS_WEBVIEW2 TRUE
)
# Link binary data
target_link_libraries(YourPlugin
PRIVATE
YourPlugin_WebUI
juce::juce_gui_extra
# ... other modules
)
# WebView2 definitions
target_compile_definitions(YourPlugin
PUBLIC
JUCE_WEB_BROWSER=1
JUCE_USE_WIN_WEBVIEW2_WITH_STATIC_LINKING=1
)
#pragma once
namespace ParameterIDs {
constexpr char GAIN[] = "GAIN";
constexpr char FREQUENCY[] = "FREQUENCY";
}
#pragma once
#include <juce_gui_extra/juce_gui_extra.h>
#include "PluginProcessor.h"
#include "ParameterIDs.hpp"
class YourPluginEditor : public juce::AudioProcessorEditor
{
public:
explicit YourPluginEditor(YourAudioProcessor&);
~YourPluginEditor() override;
void paint(juce::Graphics&) override;
void resized() override;
private:
YourAudioProcessor& audioProcessor;
// CRITICAL: Relays ā WebView ā Attachments
juce::WebSliderRelay gainRelay { ParameterIDs::GAIN };
juce::WebSliderRelay frequencyRelay { ParameterIDs::FREQUENCY };
std::unique_ptr<juce::WebBrowserComponent> webView;
std::unique_ptr<juce::WebSliderParameterAttachment> gainAttachment;
std::unique_ptr<juce::WebSliderParameterAttachment> frequencyAttachment;
std::optional<juce::WebBrowserComponent::Resource> getResource(const juce::String& url);
static const char* getMimeForExtension(const juce::String& extension);
static juce::String getExtension(juce::String filename);
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (YourPluginEditor)
};
#include "PluginEditor.h"
#include "BinaryData.h"
YourPluginEditor::YourPluginEditor(YourAudioProcessor& p)
: AudioProcessorEditor(&p), audioProcessor(p)
{
setSize(600, 400);
// Create WebView with relay references
webView = std::make_unique<juce::WebBrowserComponent>(
juce::WebBrowserComponent::Options()
.withBackend(juce::WebBrowserComponent::Options::Backend::webview2)
.withWinWebView2Options(
juce::WebBrowserComponent::Options::WinWebView2{}
.withUserDataFolder(juce::File::getSpecialLocation(
juce::File::SpecialLocationType::tempDirectory)))
.withNativeIntegrationEnabled()
.withOptionsFrom(gainRelay)
.withOptionsFrom(frequencyRelay)
.withResourceProvider([this](const auto& url) {
return getResource(url);
})
);
addAndMakeVisible(*webView);
// Create attachments (AFTER webView)
gainAttachment = std::make_unique<juce::WebSliderParameterAttachment>(
*audioProcessor.getAPVTS().getParameter(ParameterIDs::GAIN),
gainRelay,
nullptr
);
frequencyAttachment = std::make_unique<juce::WebSliderParameterAttachment>(
*audioProcessor.getAPVTS().getParameter(ParameterIDs::FREQUENCY),
frequencyRelay,
nullptr
);
// Load UI
webView->goToURL(juce::WebBrowserComponent::getResourceProviderRoot());
}
YourPluginEditor::~YourPluginEditor() {}
void YourPluginEditor::paint(juce::Graphics& g)
{
g.fillAll(getLookAndFeel().findColour(juce::ResizableWindow::backgroundColourId));
}
void YourPluginEditor::resized()
{
webView->setBounds(getLocalBounds());
}
std::optional<juce::WebBrowserComponent::Resource> YourPluginEditor::getResource(const juce::String& url)
{
const auto urlToRetrieve = url == "/" ? juce::String{ "index.html" }
: url.fromFirstOccurrenceOf("/", false, false);
// Try to find resource in BinaryData
for (int i = 0; i < BinaryData::namedResourceListSize; ++i)
{
const char* resourceName = BinaryData::namedResourceList[i];
const char* originalFilename = BinaryData::getNamedResourceOriginalFilename(resourceName);
if (originalFilename != nullptr && juce::String(originalFilename).endsWith(urlToRetrieve))
{
int dataSize = 0;
const char* data = BinaryData::getNamedResource(resourceName, dataSize);
if (data != nullptr && dataSize > 0)
{
std::vector<std::byte> byteData((size_t)dataSize);
std::memcpy(byteData.data(), data, (size_t)dataSize);
auto mime = getMimeForExtension(getExtension(urlToRetrieve).toLowerCase());
return juce::WebBrowserComponent::Resource{ std::move(byteData), juce::String{ mime } };
}
}
}
return std::nullopt;
}
const char* YourPluginEditor::getMimeForExtension(const juce::String& extension)
{
static const std::unordered_map<juce::String, const char*> mimeMap =
{
{ { "html" }, "text/html" },
{ { "css" }, "text/css" },
{ { "js" }, "text/javascript" },
{ { "json" }, "application/json" },
{ { "png" }, "image/png" },
{ { "jpg" }, "image/jpeg" },
{ { "svg" }, "image/svg+xml" }
};
if (const auto it = mimeMap.find(extension.toLowerCase()); it != mimeMap.end())
return it->second;
return "text/plain";
}
juce::String YourPluginEditor::getExtension(juce::String filename)
{
return filename.fromLastOccurrenceOf(".", false, false);
}
.\scripts\build-and-install.ps1 -PluginName YourPlugin
Before considering your WebView plugin complete:
unique_ptr)unique_ptr.withBackend(webview2) specified.withWinWebView2Options(...withUserDataFolder(...)) provided.withNativeIntegrationEnabled() included.withOptionsFrom(relay) for eachwebView->goToURL(...) calledjuce_add_binary_data()NEEDS_WEBVIEW2 TRUE setJUCE_WEB_BROWSER=1 definedJUCE_USE_WIN_WEBVIEW2_WITH_STATIC_LINKING=1 definedstd::nullopt for missing resourcessliderDragStarted() / sliderDragEnded() calledvalueChangedEvent.addListener() used for automation// WRONG - Crashes on unload!
std::unique_ptr<juce::WebBrowserComponent> webView;
juce::WebSliderRelay relay { "PARAM" };
// WRONG - Parameter binding won't work!
webView = std::make_unique<juce::WebBrowserComponent>(
Options().withBackend(webview2)
// Missing: .withOptionsFrom(gainRelay)
);
// WRONG - JS files won't execute!
return Resource{ data, "text/html" }; // For a .js file!
// WRONG - Order matters!
gainAttachment = std::make_unique<...>(...); // Too early
webView = std::make_unique<...>(...); // Too late
# WRONG - Missing JS files!
juce_add_binary_data(Plugin_WebUI
SOURCES
Source/ui/public/index.html
# Missing: js files!
)
For detailed technical information, see the reference documents:
.claude/troubleshooting/resolutions/webview-member-order-crash.mdtemplates/webview/plugins/AngelGrain/, plugins/TestWebView/.claude/troubleshooting/known-issues.yaml (webview-001, webview-002)Document Version: 2.0 (Streamlined) Last Updated: 2026-01-24 Status: Production Ready