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.
JUCE 8 WebView Plugin - Quick Start Guide
Platform: Windows 11 | JUCE 8 | WebView2 | CMake
๐ฏ Quick Overview
Build audio plugin UIs using modern web technologies (HTML/CSS/JavaScript) instead of C++ JUCE components.
Benefits:
- Fast iteration with hot reloading
- Use familiar web frameworks (React, Vue, or vanilla JS)
- Modern UI capabilities (CSS animations, flexbox, gradients)
- Team-friendly (frontend devs can work without C++ knowledge)
- GPU acceleration via WebGL
Trade-offs:
- ~100MB additional memory footprint
- Windows 11 WebView2 dependency
- Different performance characteristics than native C++
๐ด CRITICAL: Member Declaration Order (PREVENTS DAW CRASHES)
โ ๏ธ #1 CAUSE OF WEBVIEW PLUGIN CRASHES - MUST FOLLOW
The Rule
C++ destroys members in REVERSE order of declaration. WebView references relays, so relays must be declared FIRST to be destroyed LAST.
Correct Pattern (PluginEditor.h)
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
๐ Step-by-Step Implementation
Step 1: Create Web UI Files
plugins/YourPlugin/
โโโ Source/
โโโ ui/
โโโ public/
โโโ index.html
โโโ js/
โโโ index.js
Step 2: Write index.html
<!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>
Step 3: Write index.js
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);
});
});
Step 4: Configure CMakeLists.txt
# 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
)
Step 5: Create ParameterIDs.hpp
#pragma once
namespace ParameterIDs {
constexpr char GAIN[] = "GAIN";
constexpr char FREQUENCY[] = "FREQUENCY";
}
Step 6: Write PluginEditor.h
#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)
};
Step 7: Write PluginEditor.cpp
#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);
}
Step 8: Build
.\scripts\build-and-install.ps1 -PluginName YourPlugin
Step 9: Test
- Load plugin in DAW
- Open plugin window โ UI should display
- Move sliders โ parameters should update
- Automate in DAW โ UI should update
- Close window โ should NOT crash
- Unload plugin โ should NOT crash
โ Validation Checklist
Before considering your WebView plugin complete:
Code Structure
- Member order: Relays โ WebView โ Attachments
- Destruction order comment added to header
- All relays are direct members (not
unique_ptr) - WebView and attachments are
unique_ptr
WebView Setup
-
.withBackend(webview2)specified -
.withWinWebView2Options(...withUserDataFolder(...))provided -
.withNativeIntegrationEnabled()included - All relays registered:
.withOptionsFrom(relay)for each - Resource provider implemented
-
webView->goToURL(...)called
CMakeLists.txt
- All web files in
juce_add_binary_data() -
NEEDS_WEBVIEW2 TRUEset - Binary data target linked
-
JUCE_WEB_BROWSER=1defined -
JUCE_USE_WIN_WEBVIEW2_WITH_STATIC_LINKING=1defined
Resource Provider
- Handles "/" โ "index.html" mapping
- Iterates BinaryData when direct lookup fails
- Correct MIME types returned
- Returns
std::nulloptfor missing resources
JavaScript
- Parameter IDs match C++ exactly
-
sliderDragStarted()/sliderDragEnded()called -
valueChangedEvent.addListener()used for automation
Testing
- Plugin loads without errors
- UI displays correctly
- Parameters work (user interaction)
- Automation works (DAW โ UI updates)
- Window closes without crash
- Plugin unloads without crash
- Multiple instances work
โ ๏ธ Common Mistakes
โ Wrong Member Order
// WRONG - Crashes on unload!
std::unique_ptr<juce::WebBrowserComponent> webView;
juce::WebSliderRelay relay { "PARAM" };
โ Missing .withOptionsFrom()
// WRONG - Parameter binding won't work!
webView = std::make_unique<juce::WebBrowserComponent>(
Options().withBackend(webview2)
// Missing: .withOptionsFrom(gainRelay)
);
โ Wrong MIME Type
// WRONG - JS files won't execute!
return Resource{ data, "text/html" }; // For a .js file!
โ Creating Attachments Before WebView
// WRONG - Order matters!
gainAttachment = std::make_unique<...>(...); // Too early
webView = std::make_unique<...>(...); // Too late
โ Not Embedding All Files
# WRONG - Missing JS files!
juce_add_binary_data(Plugin_WebUI
SOURCES
Source/ui/public/index.html
# Missing: js files!
)
๐ Additional Resources
For detailed technical information, see the reference documents:
- Technical Details - WebView2 internals, JUCE 8 changes, architecture
- Communication Guide - Frontend-backend events, native functions
- Resource Providers - Detailed resource serving patterns
- Parameter Synchronization - Advanced parameter binding patterns
- Troubleshooting - Debugging, performance, common issues
๐ Related Documentation
- Troubleshooting:
.claude/troubleshooting/resolutions/webview-member-order-crash.md - Templates:
templates/webview/ - Working Examples:
plugins/AngelGrain/,plugins/TestWebView/ - Known Issues:
.claude/troubleshooting/known-issues.yaml(webview-001, webview-002)
Document Version: 2.0 (Streamlined) Last Updated: 2026-01-24 Status: Production Ready