| 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;
juce::WebSliderRelay gainRelay { "GAIN" };
juce::WebSliderRelay frequencyRelay { "FREQUENCY" };
std::unique_ptr<juce::WebBrowserComponent> webView;
std::unique_ptr<juce::WebSliderParameterAttachment> gainAttachment;
std::unique_ptr<juce::WebSliderParameterAttachment> frequencyAttachment;
Wrong Order → DAW Crash on Unload
See: ..kilocode/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>
< = = = = = =>
0.5
Step 3: Write index.js
import * as Juce from "./juce/index.js";
document.addEventListener("DOMContentLoaded", () => {
const gainState = Juce.getSliderState("GAIN");
const gainSlider = document.getElementById("gainSlider");
const gainValue = document.getElementById("gainValue");
gainSlider.addEventListener("mousedown", () => gainState.sliderDragStarted());
gainSlider.addEventListener("mouseup", () => gainState.sliderDragEnded());
gainSlider.addEventListener("input", () => {
gainState.setNormalisedValue(gainSlider.value);
gainValue.textContent = gainSlider.value;
});
gainState.valueChangedEvent.addListener(() => {
const value = gainState.getNormalisedValue();
gainSlider.value = value;
gainValue. = value.();
});
});
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;
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);
(YourPluginEditor)
};
Step 7: Write PluginEditor.cpp
#include "PluginEditor.h"
#include "BinaryData.h"
YourPluginEditor::YourPluginEditor(YourAudioProcessor& p)
: AudioProcessorEditor(&p), audioProcessor(p)
{
setSize(600, 400);
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);
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
);
webView->(juce::WebBrowserComponent::());
}
YourPluginEditor::~() {}
{
g.(().(juce::ResizableWindow::backgroundColourId));
}
{
webView->(());
}
{
urlToRetrieve = url == ? juce::String{ }
: url.(, , );
( i = ; i < BinaryData::namedResourceListSize; ++i)
{
* resourceName = BinaryData::namedResourceList[i];
* originalFilename = BinaryData::(resourceName);
(originalFilename != && juce::(originalFilename).(urlToRetrieve))
{
dataSize = ;
* data = BinaryData::(resourceName, dataSize);
(data != && dataSize > )
{
;
std::(byteData.(), data, ()dataSize);
mime = ((urlToRetrieve).());
juce::WebBrowserComponent::Resource{ std::(byteData), juce::String{ mime } };
}
}
}
std::;
}
{
std::unordered_map<juce::String, *> mimeMap =
{
{ { }, },
{ { }, },
{ { }, },
{ { }, },
{ { }, },
{ { }, },
{ { }, }
};
( it = mimeMap.(extension.()); it != mimeMap.())
it->second;
;
}
{
filename.(, , );
}
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
WebView Setup
CMakeLists.txt
Resource Provider
JavaScript
Testing
⚠️ Common Mistakes
❌ Wrong Member Order
std::unique_ptr<juce::WebBrowserComponent> webView;
juce::WebSliderRelay relay { "PARAM" };
❌ Missing .withOptionsFrom()
webView = std::make_unique<juce::WebBrowserComponent>(
Options().withBackend(webview2)
);
❌ Wrong MIME Type
return Resource{ data, "text/html" };
❌ Creating Attachments Before WebView
gainAttachment = std::make_unique<...>(...);
webView = std::make_unique<...>(...);
❌ 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:
🔗 Related Documentation
- Troubleshooting:
..kilocode/troubleshooting/resolutions/webview-member-order-crash.md
- Templates:
templates/webview/
- Working Examples:
plugins/AngelGrain/, plugins/TestWebView/
- Known Issues:
..kilocode/troubleshooting/known-issues.yaml (webview-001, webview-002)
Document Version: 2.0 (Streamlined)
Last Updated: 2026-01-24
Status: Production Ready