matlab-uihtml-app-builder — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited matlab-uihtml-app-builder (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
This skill covers how to build interactive web applications that combine HTML/JavaScript interfaces with MATLAB computational backends using the uihtml component. The HTML side handles the UI; MATLAB does the computation.
The uihtml component enables bidirectional communication between JavaScript and MATLAB through several mechanisms:
#### Pattern 1: MATLAB → JavaScript (Data Property)
Use Case: Sending data from MATLAB to update the HTML interface
% MATLAB side
h.Data = "Hello World!";// JavaScript side
htmlComponent.addEventListener("DataChanged", function(event) {
document.getElementById("display").innerHTML = htmlComponent.Data;
});#### Pattern 2: JavaScript → MATLAB (Events)
Use Case: Triggering MATLAB functions from user interactions
// JavaScript side - send event to MATLAB
htmlComponent.sendEventToMATLAB("Calculate", expression);% MATLAB side - receive and handle event
h.HTMLEventReceivedFcn = @handleEvent;
function handleEvent(src, event)
eventName = event.HTMLEventName;
eventData = event.HTMLEventData;
% Process event...
end#### Pattern 3: MATLAB → JavaScript (Custom Events)
Use Case: Sending computed results or status updates to JavaScript
% MATLAB side - send custom event to JavaScript
sendEventToHTMLSource(h, "ResultChanged", result);// JavaScript side - listen for custom event
htmlComponent.addEventListener("ResultChanged", function(event) {
document.getElementById("display").textContent = event.Data;
});#### Pattern 4: Complex Data Transfer
Use Case: Passing structured data between MATLAB and JavaScript
% MATLAB side - struct data gets JSON encoded automatically
itemData = struct("ItemName","Apple","Price",2,"Quantity",10);
h.Data = itemData;// JavaScript side - access as object properties
htmlComponent.Data.ItemName // "Apple"
htmlComponent.Data.Price // 2
htmlComponent.Data.Quantity // 10Important: decoding is automatic in both directions.
sendEventToMATLAB arrives on event.HTMLEventData already converted to a MATLAB struct. Do not call `jsondecode`; it will fail on a struct.sendEventToHTMLSource arrives on event.Data already as a JavaScript object. Do not call `JSON.parse`; it will fail on an object.{x0: 1} becomes a MATLAB struct('x0', 1), not struct('x_0', ...) or similar.double. Wrap field reads with double(data.x0) if you want to be defensive about types.HTMLSource = 'trusted' when using local HTML files: h.HTMLSource = fullfile(pwd, 'myapp.html');
% This is treated as trusted automatically for local fileseval() on user input without strict sanitizationALWAYS wrap MATLAB event handlers in try-catch blocks:
function handleEvent(src, event)
eventName = event.HTMLEventName;
eventData = event.HTMLEventData;
try
% Process the event
result = processData(eventData);
% Send result back to JavaScript
sendEventToHTMLSource(src, 'ResultEvent', result);
catch ME
% Handle errors gracefully
fprintf('Error: %s\n', ME.message);
sendEventToHTMLSource(src, 'ErrorEvent', ME.message);
end
endALWAYS validate user input before processing:
function result = validateExpression(expression)
allowedChars = '0123456789+-*/.() ';
if ~all(ismember(expression, allowedChars))
error('Invalid characters in expression');
end
% Additional validation...
result = true;
endFollow this directory structure:
project/
├── app.m # Main MATLAB function
├── app.html # HTML interface
├── README.md # Usage instructions
└── examples/ # Additional examples (optional)MATLAB Side (calculator.m):
function calculator()
% Create main figure
fig = uifigure('Name', 'Calculator', 'Position', [100 100 400 500]);
% Create HTML component
h = uihtml(fig, 'Position', [25 25 350 450]);
h.HTMLSource = fullfile(pwd, 'calculator.html');
h.HTMLEventReceivedFcn = @(src, event) handleEvent(src, event);
end
function handleEvent(src, event)
eventName = event.HTMLEventName;
eventData = event.HTMLEventData;
try
switch eventName
case 'Calculate'
% Validate input
expression = char(eventData);
allowedChars = '0123456789+-*/.() ';
if ~all(ismember(expression, allowedChars))
error('Invalid characters in expression');
end
% Evaluate safely
result = eval(expression);
% Send result back
sendEventToHTMLSource(src, 'Result', num2str(result));
case 'Clear'
sendEventToHTMLSource(src, 'Result', '0');
end
catch ME
fprintf('Error: %s\n', ME.message);
sendEventToHTMLSource(src, 'Error', 'Invalid expression');
end
endHTML Side (calculator.html):
<!DOCTYPE html>
<html>
<head>
<style>
body {
font-family: Arial, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
margin: 0;
padding: 20px;
}
.calculator {
background: white;
border-radius: 10px;
padding: 20px;
box-shadow: 0 10px 30px rgba(0,0,0,0.3);
}
.display {
width: 100%;
height: 60px;
font-size: 24px;
text-align: right;
padding: 10px;
border: 2px solid #ccc;
border-radius: 5px;
margin-bottom: 10px;
background: #f9f9f9;
}
.buttons {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 10px;
}
button {
padding: 20px;
font-size: 18px;
border: none;
border-radius: 5px;
cursor: pointer;
background: #667eea;
color: white;
transition: background 0.3s;
}
button:hover {
background: #764ba2;
}
.operator {
background: #ff6b6b;
}
.operator:hover {
background: #ee5a52;
}
</style>
<script type="text/javascript">
let currentExpression = '';
function setup(htmlComponent) {
window.htmlComponent = htmlComponent;
// Listen for results from MATLAB
htmlComponent.addEventListener("Result", function(event) {
document.getElementById("display").value = event.Data;
currentExpression = event.Data;
});
htmlComponent.addEventListener("Error", function(event) {
document.getElementById("display").value = "Error";
currentExpression = '';
});
}
function appendToDisplay(value) {
currentExpression += value;
document.getElementById("display").value = currentExpression;
}
function clearDisplay() {
currentExpression = '';
document.getElementById("display").value = '0';
window.htmlComponent.sendEventToMATLAB("Clear", "");
}
function calculate() {
if (currentExpression) {
window.htmlComponent.sendEventToMATLAB("Calculate", currentExpression);
}
}
</script>
</head>
<body>
<div class="calculator">
<input type="text" id="display" class="display" value="0" readonly>
<div class="buttons">
<button onclick="appendToDisplay('7')">7</button>
<button onclick="appendToDisplay('8')">8</button>
<button onclick="appendToDisplay('9')">9</button>
<button class="operator" onclick="appendToDisplay('/')">/</button>
<button onclick="appendToDisplay('4')">4</button>
<button onclick="appendToDisplay('5')">5</button>
<button onclick="appendToDisplay('6')">6</button>
<button class="operator" onclick="appendToDisplay('*')">*</button>
<button onclick="appendToDisplay('1')">1</button>
<button onclick="appendToDisplay('2')">2</button>
<button onclick="appendToDisplay('3')">3</button>
<button class="operator" onclick="appendToDisplay('-')">-</button>
<button onclick="appendToDisplay('0')">0</button>
<button onclick="appendToDisplay('.')">.</button>
<button onclick="calculate()">=</button>
<button class="operator" onclick="appendToDisplay('+')">+</button>
<button style="grid-column: span 4; background: #ff6b6b;" onclick="clearDisplay()">Clear</button>
</div>
</div>
</body>
</html>MATLAB Side (visualizer.m):
function visualizer()
fig = uifigure('Name', 'Data Visualizer', 'Position', [100 100 800 600]);
% Create HTML component for controls
h = uihtml(fig, 'Position', [25 400 750 175]);
h.HTMLSource = fullfile(pwd, 'controls.html');
h.HTMLEventReceivedFcn = @(src, event) handleEvent(src, event, fig);
% Create axes for plotting
ax = uiaxes(fig, 'Position', [25 25 750 350]);
xlabel(ax, 'X');
ylabel(ax, 'Y');
title(ax, 'Interactive Plot');
end
function handleEvent(src, event, fig)
eventName = event.HTMLEventName;
eventData = event.HTMLEventData;
try
switch eventName
case 'UpdatePlot'
% Parse parameters from JavaScript
params = eventData;
frequency = params.frequency;
amplitude = params.amplitude;
plotType = params.plotType;
% Generate data
x = linspace(0, 4*pi, 200);
switch plotType
case 'sine'
y = amplitude * sin(frequency * x);
case 'cosine'
y = amplitude * cos(frequency * x);
case 'both'
y = amplitude * sin(frequency * x);
y2 = amplitude * cos(frequency * x);
end
% Find axes and plot
ax = findobj(fig, 'Type', 'axes');
cla(ax);
if strcmp(plotType, 'both')
plot(ax, x, y, 'LineWidth', 2);
hold(ax, 'on');
plot(ax, x, y2, 'LineWidth', 2);
hold(ax, 'off');
legend(ax, 'Sine', 'Cosine');
else
plot(ax, x, y, 'LineWidth', 2);
end
grid(ax, 'on');
% Send confirmation
sendEventToHTMLSource(src, 'PlotUpdated', 'Success');
end
catch ME
fprintf('Error: %s\n', ME.message);
sendEventToHTMLSource(src, 'Error', ME.message);
end
endHTML Side (controls.html):
<!DOCTYPE html>
<html>
<head>
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #2c3e50 0%, #34495e 100%);
color: white;
margin: 0;
padding: 20px;
}
.controls {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 20px;
}
.control-group {
background: rgba(255,255,255,0.1);
padding: 15px;
border-radius: 8px;
}
label {
display: block;
margin-bottom: 5px;
font-weight: bold;
}
input[type="range"] {
width: 100%;
}
select, button {
width: 100%;
padding: 8px;
border-radius: 5px;
border: none;
font-size: 14px;
}
button {
background: #3498db;
color: white;
cursor: pointer;
margin-top: 10px;
transition: background 0.3s;
}
button:hover {
background: #2980b9;
}
</style>
<script type="text/javascript">
function setup(htmlComponent) {
window.htmlComponent = htmlComponent;
htmlComponent.addEventListener("PlotUpdated", function(event) {
console.log("Plot updated successfully");
});
}
function updatePlot() {
const frequency = parseFloat(document.getElementById("frequency").value);
const amplitude = parseFloat(document.getElementById("amplitude").value);
const plotType = document.getElementById("plotType").value;
const params = {
frequency: frequency,
amplitude: amplitude,
plotType: plotType
};
window.htmlComponent.sendEventToMATLAB("UpdatePlot", params);
}
function updateFreqLabel(value) {
document.getElementById("freqValue").textContent = value;
}
function updateAmpLabel(value) {
document.getElementById("ampValue").textContent = value;
}
</script>
</head>
<body>
<div class="controls">
<div class="control-group">
<label>Frequency: <span id="freqValue">1</span></label>
<input type="range" id="frequency" min="0.1" max="5" step="0.1" value="1"
oninput="updateFreqLabel(this.value)">
</div>
<div class="control-group">
<label>Amplitude: <span id="ampValue">1</span></label>
<input type="range" id="amplitude" min="0.1" max="5" step="0.1" value="1"
oninput="updateAmpLabel(this.value)">
</div>
<div class="control-group">
<label>Plot Type:</label>
<select id="plotType">
<option value="sine">Sine</option>
<option value="cosine">Cosine</option>
<option value="both">Both</option>
</select>
</div>
</div>
<button onclick="updatePlot()">Update Plot</button>
</body>
</html>MATLAB Side (formProcessor.m):
function formProcessor()
fig = uifigure('Name', 'Form Processor', 'Position', [100 100 600 400]);
h = uihtml(fig, 'Position', [25 25 550 350]);
h.HTMLSource = fullfile(pwd, 'form.html');
h.HTMLEventReceivedFcn = @(src, event) handleEvent(src, event);
end
function handleEvent(src, event)
eventName = event.HTMLEventName;
eventData = event.HTMLEventData;
try
switch eventName
case 'SubmitForm'
% Extract form data
name = eventData.name;
email = eventData.email;
age = eventData.age;
% Validate data
if isempty(name) || isempty(email)
error('Name and email are required');
end
if ~contains(email, '@')
error('Invalid email address');
end
if age < 0 || age > 120
error('Invalid age');
end
% Process data (example: save to file or database)
fprintf('Processing form:\n');
fprintf(' Name: %s\n', name);
fprintf(' Email: %s\n', email);
fprintf(' Age: %d\n', age);
% Send success message
result = struct('status', 'success', ...
'message', 'Form submitted successfully!');
sendEventToHTMLSource(src, 'FormResult', result);
case 'ClearForm'
sendEventToHTMLSource(src, 'FormCleared', '');
end
catch ME
fprintf('Error: %s\n', ME.message);
result = struct('status', 'error', 'message', ME.message);
sendEventToHTMLSource(src, 'FormResult', result);
end
endmatlab-uihtml-design for ready-made styles)JavaScript Side:
htmlComponent.addEventListener("Error", function(event) {
// Display user-friendly error messages
alert("Error: " + event.Data);
});MATLAB Side:
try
result = processInput(input);
sendEventToHTMLSource(src, 'Success', result);
catch ME
fprintf('Error: %s\n', ME.message);
sendEventToHTMLSource(src, 'Error', 'Processing failed');
end % Test individual processing functions
assert(validateExpression('2+2'), 'Validation should pass'); % Test event handling with sample data
testEvent = struct('HTMLEventName', 'Calculate', 'HTMLEventData', '2+2');
handleEvent(h, testEvent);fprintf() to log events and data fprintf('Received event: %s with data: %s\n', eventName, eventData); console.log("Sending to MATLAB:", data); fprintf('Data type: %s\n', class(eventData));
fprintf('Data value: %s\n', string(eventData));sendEventToMATLABsendEventToHTMLSourcesendEventToHTMLSource at intervalsWhen MATLAB drives an operation that takes more than a fraction of a second (an animation, an integration sweep, a streaming simulation), the patterns above need a few additions: a timer, resource cleanup, a way to cancel, and a way to tune the operation while it runs.
Use a MATLAB timer to drive the operation. Store the handle on fig.UserData so other event handlers (Reset, Stop, figure-close) can find and stop it.
function startAnimation(fig, ax, data)
stopAnimTimer(fig); % kill any prior run
state.data = data;
state.idx = 1;
state.line = animatedline(ax, 'Color', [0.65 0.55 0.94], 'LineWidth', 1.4);
state.step = 5; % points per tick (mutable from outside; see below)
tmr = timer( ...
'ExecutionMode', 'fixedSpacing', ...
'Period', 0.03, ...
'BusyMode', 'drop', ... % skip ticks if callback overruns
'TimerFcn', @(s, ~) animStep(s, fig));
tmr.UserData = state;
fig.UserData.AnimTimer = tmr;
start(tmr);
end
function animStep(tmr, fig)
if ~isvalid(fig) || ~isvalid(tmr), return; end
s = tmr.UserData;
iEnd = min(numel(s.data), s.idx + s.step - 1);
addpoints(s.line, s.data(s.idx:iEnd, 1), s.data(s.idx:iEnd, 2));
s.idx = iEnd + 1;
tmr.UserData = s;
drawnow limitrate;
if s.idx > numel(s.data)
stop(tmr); delete(tmr);
fig.UserData.AnimTimer = [];
% Notify JS that the operation completed
sendEventToHTMLSource(fig.UserData.UIHtml, 'OperationDone', struct('n', iEnd));
end
endKey choices:
Period, the next tick is skipped rather than queued. This prevents callback pile-ups on slow systems.Any timer, listener, or background resource must be torn down when the figure closes. Otherwise it leaks across reruns and can fire callbacks against deleted graphics objects.
fig.UserData = struct('AnimTimer', [], 'UIHtml', h, 'Axes', ax);
fig.CloseRequestFcn = @(s, ~) closeFig(s);
function closeFig(fig)
stopAnimTimer(fig);
delete(fig);
end
function stopAnimTimer(fig)
if ~isvalid(fig) || ~isfield(fig.UserData, 'AnimTimer'), return; end
tmr = fig.UserData.AnimTimer;
if ~isempty(tmr) && isvalid(tmr)
try, stop(tmr); catch, end
delete(tmr);
end
fig.UserData.AnimTimer = [];
endThe same stopAnimTimer is reused by Reset and Stop handlers, so there's one cleanup path to maintain.
For operations the user might want to cancel, repurpose the primary action button via a JS state machine. Don't add a separate Stop button; the panel gets crowded, and a context-aware single button matches play/pause UX.
// JS state machine
let runState = "idle"; // 'idle' | 'running' | 'cancellable'
function setRunning(state) {
runState = state;
const b = document.getElementById("runBtn");
b.classList.remove("busy");
if (state === "idle") {
b.textContent = "Run";
b.disabled = false;
} else if (state === "running") { // brief, non-cancellable
b.textContent = "Run";
b.classList.add("busy");
b.disabled = true;
} else if (state === "cancellable") { // long, user can stop
b.textContent = "Stop";
b.classList.add("busy");
b.disabled = false;
}
}
function onRunClick() {
if (runState === "cancellable") {
window.htmlComponent.sendEventToMATLAB("StopOperation", "");
return;
}
setRunning("cancellable");
window.htmlComponent.sendEventToMATLAB("RunOperation", collectParams());
}On the MATLAB side, StopOperation stops the timer, leaves any partial output in place (so the user keeps what was computed), and emits an event so JS can flip the button back to "Run":
case 'StopOperation'
stopAnimTimer(fig);
s = lastState(fig); % whatever partial result you tracked
sendEventToHTMLSource(src, 'OperationStopped', s);Distinguish three completion events back to JS:
| Event | When |
|---|---|
OperationDone | Natural completion (full result) |
OperationStopped | User clicked Stop (partial result) |
OperationError | Caught exception (error message) |
All three should call setRunning("idle") in JS so the button returns to "Run".
When the user wants to tune a parameter while the operation runs (animation speed, simulation rate, plot range), the JS slider's input event needs to push updates to MATLAB without restarting the operation.
Two rules:
let sendTimer = null;
document.getElementById("speed").addEventListener("input", function() {
if (sendTimer) return;
sendTimer = setTimeout(function() {
const v = parseInt(document.getElementById("speed").value, 10);
window.htmlComponent.sendEventToMATLAB("SetSpeed", v);
sendTimer = null;
}, 80); // ~12 events/sec max
}); case 'SetSpeed'
tmr = fig.UserData.AnimTimer;
if ~isempty(tmr) && isvalid(tmr)
s = tmr.UserData;
s.step = max(1, min(50, round(double(data)))); % clamp + sanitize
tmr.UserData = s;
end
% no-op if no animation running; the slider still updates its labelMATLAB serializes all callbacks on the main thread, so the timer tick and the SetSpeed handler never execute concurrently. The UserData update is atomic from the tick's point of view.
A common requirement: a "Save Image" button in the HTML that exports the current plot. Use uiputfile + exportgraphics:
case 'ExportImage'
ax = fig.UserData.Axes;
if isempty(ax.Children)
sendEventToHTMLSource(src, 'ExportError', 'Nothing to export');
return;
end
defaultName = sprintf('plot_%s.png', string(datetime('now', 'Format', 'yyyyMMdd_HHmmss')));
[file, path] = uiputfile({'*.png';'*.jpg';'*.pdf'}, 'Export plot', defaultName);
if isequal(file, 0)
sendEventToHTMLSource(src, 'ExportComplete', 'cancelled');
return;
end
try
exportgraphics(ax, fullfile(path, file), 'Resolution', 300, ...
'BackgroundColor', fig.Color);
sendEventToHTMLSource(src, 'ExportComplete', file);
catch ME
sendEventToHTMLSource(src, 'ExportError', ME.message);
endPass fig.Color as BackgroundColor so the export matches the on-screen figure background. Important for dark themes, where the default white background looks wrong.
Before deploying a uihtml app, verify:
HTMLSource property set to correct file pathHTMLEventReceivedFcn callback definedsetup(htmlComponent) function implementedmatlab-uihtml-design)timer, listener, or background task is stopped and deleted when the figure closes. Without this, callbacks fire against deleted graphics after the user closes the app.Issue: HTML file not loading in uihtml component
h.HTMLSource = fullfile(pwd, 'app.html'); % Absolute pathIssue: Events not triggering MATLAB callback
HTMLEventReceivedFcn is set before HTML loadssendEventToMATLAB correctlyIssue: Data not updating in JavaScript
DataChanged event listener is registered in setup()h.Data property, not sending eventIssue: JavaScript errors in browser console
htmlComponent is passed to setup() functionIssue: MATLAB errors not displayed to user
sendEventToHTMLSourceIssue: Slow performance when sending data
Issue: Complex data structures not transferring correctly
Issue: Styling not appearing correctly
<style> block inside <head>doc uihtmldoc sendEventToHTMLSourcedoc uifigure~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.