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.
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
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 // 10
Important: decoding is automatic in both directions.
A JS object sent via sendEventToMATLAB arrives on event.HTMLEventData already converted to a MATLAB struct. Do not call jsondecode; it will fail on a struct.
A MATLAB struct sent via sendEventToHTMLSource arrives on event.Data already as a JavaScript object. Do not call JSON.parse; it will fail on an object.
Field names round-trip exactly: a JS {x0: 1} becomes a MATLAB struct('x0', 1), not struct('x_0', ...) or similar.
Numeric scalars arrive as MATLAB double. Wrap field reads with double(data.x0) if you want to be defensive about types.
ALWAYS set HTMLSource to a fixed path of an HTML file shipped next to the app code, and load only local scripts that ship with the app:
h.HTMLSource = fullfile(pwd, 'myapp.html');
ALWAYS resolve the HTML path from the folder that holds the app file rather than the current working folder, so only the shipped file is loaded even when MATLAB runs from a shared folder
Any loaded HTML can trigger MATLAB handlers, so every handler must validate its input
MUST validate all input from JavaScript before processing in MATLAB
ALWAYS keep data sent from JavaScript away from eval() and any other function that runs text as code, even after filtering; use a small arithmetic parser with length and nesting limits instead
ALWAYS display Data and event values through the textContent property, and build any markup with element creation calls
ALWAYS restrict allowed characters in user input for expressions
ALWAYS 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, 'Error', 'Processing failed');
end
end
function result = validateExpression(expression)
allowedChars = '0123456789+-*/.() ';
if ~all(ismember(expression, allowedChars))
error('Invalid characters in expression');
end
% Additional validation...
result = true;
end
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
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
end
The eval call here is kept only to show the wiring. The allowlist blocks letters but not very long or deeply nested input, so a real app should replace eval with a small arithmetic parser that limits expression length and nesting depth.
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');
% Send success message
result = struct('status', 'success', ...
'message', 'Form submitted');
sendEventToHTMLSource(src, 'FormResult', result);
case 'ClearForm'
sendEventToHTMLSource(src, 'FormCleared', '');
end
catch ME
fprintf('Error: %s\n', ME.message);
result = struct('status', 'error', 'message', 'Processing failed');
sendEventToHTMLSource(src, 'FormResult', result);
end
end
Before using event data in Examples 2 and 3, check that it is a struct with each expected field, that numbers are numeric, scalar and finite and clamped to the slider range, and that text fields have a length limit. Accept plotType only from the fixed list of sine, cosine and both, and raise an error for anything else.
Keep personal data such as name, email and age out of logs. Collect only the fields the task needs, get the user's consent, and protect any personal data saved to a file or database.
The HTML in the examples below is deliberately minimal to keep the focus on the MATLAB↔JS wiring. The inline styling (Arial, #667eea → #764ba2 purple-blue gradients, generic cards) is not a design reference. Those are exactly the "AI slop" tells to avoid. For any panel a user will actually see, take the visual system from matlab-uihtml-design and follow its Design Guardrails.
Use CSS Grid or Flexbox for responsive layouts that adapt to different window sizes. Flexbox for 1D, Grid for 2D; don't default to Grid when flex-wrap is simpler
Implement hover effects for better user experience and visual feedback
Provide clear visual feedback for user actions (button clicks, form submission, errors), plus a visible :focus-visible state for keyboard users
Use semantic HTML elements (button, input, form) for better accessibility
Pick a color scheme deliberately (see matlab-uihtml-design for ready-made styles). Avoid the default reflexes: Inter/Roboto/Arial fonts, purple-to-blue gradients, side-stripe border-left accents, gradient text, and over-rounded (≥32px) cards
Verify contrast: body/label text ≥4.5:1, large text and UI boundaries ≥3:1. Give numeric readouts font-variant-numeric: tabular-nums so values don't jitter width
Respect reduced motion: wrap non-essential animation in @media (prefers-reduced-motion: reduce) with a crossfade/instant fallback
Minimize data transfer between HTML and MATLAB - send only necessary data
Use appropriate data types - numbers, strings, structs (converted to JSON)
Implement loading indicators for long MATLAB operations
Cache results when appropriate using persistent variables in MATLAB
Batch multiple updates instead of sending many small events
Limit request rate: in MATLAB, ignore a new run request while a timer is already running and keep a minimum gap between compute requests; in JavaScript, disable the triggering button while a request is pending, as setRunning already does
JavaScript builds expression strings from button clicks
Send expression to MATLAB via sendEventToMATLAB
MATLAB checks the expression against a strict allowlist; a real app should use a small arithmetic parser with length and nesting limits instead of eval
When 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.
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 = [];
end
The 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:
Throttle the JS-side send so dragging the slider doesn't flood MATLAB with events:
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
});
Patch the live timer's UserData in place. Don't stop and restart:
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 label
MATLAB 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', 'Processing failed');
end
Allow only one export dialog at a time, ignore repeated export requests during a short cooldown, and write only when the chosen file type is PNG, JPG or PDF.
Pass fig.Color as BackgroundColor so the export matches the on-screen figure background. Important for dark themes, where the default white background looks wrong.
Keyboard focus is visible (:focus-visible), not removed with a bare outline: none
Responsive design tested at different window sizes
All user interactions provide visual feedback
Loading indicators shown for long operations
File organization follows project structure
Documentation (README) created with usage instructions
Resource cleanup wired to CloseRequestFcn: any 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.
Long operations are cancellable: if the operation can run more than ~1 second, the user can stop it without killing the figure.
matlab-uihtml-design: visual design styles (Clean, Material, Cosmic Dark, etc.) with ready-to-use HTML templates and design specs. Use it to style your uihtml controls. Review its templates before use; they must still follow the rules above, meaning the textContent property for event values, keeping eval away from event data, and validated events.