matlab-uihtml-app-builder

$npx mdskill add matlab/agent-skills-playground/matlab-uihtml-app-builder

Build interactive web UIs with HTML/JavaScript and MATLAB backends.

  • Create responsive web interfaces for MATLAB applications.
  • Depends on MATLAB uihtml component and HTML/JavaScript.
  • Decides based on user request for HTML, JavaScript, or web UI.
  • Delivers a working HTML/JavaScript interface with MATLAB computation.

SKILL.md

.github/skills/matlab-uihtml-app-builderView on GitHub ↗
---
name: matlab-uihtml-app-builder
description: Build interactive web applications using HTML/JavaScript interfaces with MATLAB computational backends via the uihtml component. Use when creating HTML-based MATLAB apps, JavaScript MATLAB interfaces, web UIs with MATLAB, interactive MATLAB GUIs, or when user mentions uihtml, HTML, JavaScript, web apps, or web interfaces.
license: MathWorks BSD-3-Clause (see LICENSE)
metadata:
  author: MathWorks
  version: "1.0"
---

# MATLAB uihtml App Builder

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.

## When to Use This Skill

- Building interactive MATLAB apps with HTML/JavaScript interfaces
- Creating web-based UIs for MATLAB applications
- Building responsive MATLAB GUIs with HTML/CSS/JS
- When user mentions: uihtml, HTML, JavaScript, web app, web interface, interactive GUI
- Combining web UI design with MATLAB computational power
- Creating calculator apps, data visualizers, or form-based MATLAB tools

## Core Architecture

### The Four Components

1. **HTML Interface** - User interface with buttons, forms, displays
2. **JavaScript Logic** - Event handling and UI interactions
3. **MATLAB Backend** - Computational engine and data processing
4. **uihtml Component** - Bridge between HTML and MATLAB

### Communication Patterns

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
% MATLAB side
h.Data = "Hello World!";
```

```javascript
// 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
// JavaScript side - send event to MATLAB
htmlComponent.sendEventToMATLAB("Calculate", expression);
```

```matlab
% 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
% MATLAB side - send custom event to JavaScript
sendEventToHTMLSource(h, "ResultChanged", result);
```

```javascript
// 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
% MATLAB side - struct data gets JSON encoded automatically
itemData = struct("ItemName","Apple","Price",2,"Quantity",10);
h.Data = itemData;
```

```javascript
// 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.

## Critical Rules

### Security Requirements

- **ALWAYS** set `HTMLSource = 'trusted'` when using local HTML files:
  ```matlab
  h.HTMLSource = fullfile(pwd, 'myapp.html');
  % This is treated as trusted automatically for local files
  ```

- **MUST** validate all input from JavaScript before processing in MATLAB
- **NEVER** use `eval()` on user input without strict sanitization
- **ALWAYS** restrict allowed characters in user input for expressions

### Error Handling

**ALWAYS wrap MATLAB event handlers in try-catch blocks:**

```matlab
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
end
```

### Data Validation

**ALWAYS validate user input before processing:**

```matlab
function result = validateExpression(expression)
    allowedChars = '0123456789+-*/.() ';
    if ~all(ismember(expression, allowedChars))
        error('Invalid characters in expression');
    end
    % Additional validation...
    result = true;
end
```

### File Organization

**Follow this directory structure:**

```
project/
├── app.m           # Main MATLAB function
├── app.html        # HTML interface
├── README.md       # Usage instructions
└── examples/       # Additional examples (optional)
```

## Complete Examples

### Example 1: Simple Calculator App

**MATLAB Side (calculator.m):**

```matlab
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
end
```

**HTML Side (calculator.html):**

```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>
```

### Example 2: Data Visualization App

**MATLAB Side (visualizer.m):**

```matlab
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
end
```

**HTML Side (controls.html):**

```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>
```

### Example 3: Form Processing App

**MATLAB Side (formProcessor.m):**

```matlab
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');
                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
end
```

## Best Practices

### UI Design Principles

> 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

### Performance Optimization

- **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

### Error Handling Strategy

**JavaScript Side:**
```javascript
htmlComponent.addEventListener("Error", function(event) {
    // Display user-friendly error messages
    alert("Error: " + event.Data);
});
```

**MATLAB Side:**
```matlab
try
    result = processInput(input);
    sendEventToHTMLSource(src, 'Success', result);
catch ME
    fprintf('Error: %s\n', ME.message);
    sendEventToHTMLSource(src, 'Error', 'Processing failed');
end
```

### Testing Strategy

1. **Unit Testing** - Test MATLAB functions independently
   ```matlab
   % Test individual processing functions
   assert(validateExpression('2+2'), 'Validation should pass');
   ```

2. **Integration Testing** - Test HTML-MATLAB communication
   ```matlab
   % Test event handling with sample data
   testEvent = struct('HTMLEventName', 'Calculate', 'HTMLEventData', '2+2');
   handleEvent(h, testEvent);
   ```

3. **User Testing** - Test complete user workflows
   - Try all button combinations
   - Test edge cases and invalid inputs
   - Verify visual feedback is clear

4. **Error Testing** - Test error conditions
   - Invalid input characters
   - Empty input fields
   - Network/timeout scenarios

### Debugging Tips

- **MATLAB Side**: Use `fprintf()` to log events and data
  ```matlab
  fprintf('Received event: %s with data: %s\n', eventName, eventData);
  ```

- **JavaScript Side**: Use browser developer tools (F12) to debug
  ```javascript
  console.log("Sending to MATLAB:", data);
  ```

- **Test each communication direction separately**
  - First test MATLAB → JavaScript (Data property)
  - Then test JavaScript → MATLAB (events)
  - Finally test bidirectional flow

- **Verify data types and formats**
  ```matlab
  fprintf('Data type: %s\n', class(eventData));
  fprintf('Data value: %s\n', string(eventData));
  ```

## Common Patterns

### Pattern 1: Calculator Pattern
- JavaScript builds expression strings from button clicks
- Send expression to MATLAB via `sendEventToMATLAB`
- MATLAB safely evaluates with input validation
- Results sent back via `sendEventToHTMLSource`
- Display results in real-time

### Pattern 2: Data Visualization Pattern
- JavaScript handles user interaction (sliders, dropdowns)
- Send parameters to MATLAB for computation
- MATLAB processes data and updates plots
- Can use uiaxes for MATLAB plots or send data for JavaScript plotting
- Support real-time updates and animations

### Pattern 3: Form Processing Pattern
- JavaScript collects form data into structured object
- Send entire form data as single event
- MATLAB validates each field
- Process data (save, compute, export)
- Send confirmation or error messages back
- Update UI based on results

### Pattern 4: Real-time Monitoring Pattern
- MATLAB continuously generates data (simulation, sensor reading)
- Send updates via `sendEventToHTMLSource` at intervals
- JavaScript updates display in real-time
- Implement start/stop/pause controls
- Use efficient data formats (arrays, structs)

## Long-Running Operations

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.

### Timer-Driven Animation

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.

```matlab
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
end
```

Key choices:
- **`BusyMode='drop'`**: if a callback takes longer than `Period`, the next tick is skipped rather than queued. This prevents callback pile-ups on slow systems.
- **`drawnow limitrate`**: caps rendering at ~20 fps while still processing UI events, so the user can still interact (scroll, drag sliders, click Stop).
- **Self-cleaning**: the last tick stops and deletes the timer, then nulls out the stored handle.

### Resource Cleanup on Figure Close

**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.

```matlab
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.

### Cancellable Operations (Run ↔ Stop)

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.

```javascript
// 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":

```matlab
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".

### Live Control During an Ongoing Operation

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:

1. **Throttle** the JS-side send so dragging the slider doesn't flood MATLAB with events:

   ```javascript
   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
   });
   ```

2. **Patch the live timer's `UserData` in place.** Don't stop and restart:

   ```matlab
   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.

### Saving the Output (Export from a uiaxes)

A common requirement: a "Save Image" button in the HTML that exports the current plot. Use `uiputfile` + `exportgraphics`:

```matlab
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);
    end
```

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.

## Implementation Checklist

Before deploying a uihtml app, verify:

- [ ] HTML file exists in correct location
- [ ] `HTMLSource` property set to correct file path
- [ ] `HTMLEventReceivedFcn` callback defined
- [ ] JavaScript `setup(htmlComponent)` function implemented
- [ ] Event listeners added for MATLAB→JS communication
- [ ] Try-catch blocks wrap all MATLAB event handling
- [ ] Input validation implemented for all user data
- [ ] Error events sent back to JavaScript for user feedback
- [ ] CSS styling applied consistently (see `matlab-uihtml-design`), free of AI-slop tells (purple-blue gradients, side-stripe borders, gradient text, over-rounded cards, generic Arial/Inter)
- [ ] Contrast verified (body/label ≥4.5:1, large text and UI boundaries ≥3:1)
- [ ] Reduced-motion fallback present (`@media (prefers-reduced-motion: reduce)`)
- [ ] 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.

## Troubleshooting

**Issue**: HTML file not loading in uihtml component
- **Solution**: Check file path is absolute or relative to current directory
  ```matlab
  h.HTMLSource = fullfile(pwd, 'app.html');  % Absolute path
  ```

**Issue**: Events not triggering MATLAB callback
- **Solution**: Verify `HTMLEventReceivedFcn` is set before HTML loads
- **Solution**: Check JavaScript is calling `sendEventToMATLAB` correctly

**Issue**: Data not updating in JavaScript
- **Solution**: Ensure `DataChanged` event listener is registered in `setup()`
- **Solution**: Verify MATLAB is setting `h.Data` property, not sending event

**Issue**: JavaScript errors in browser console
- **Solution**: Open browser dev tools (F12) to see detailed error messages
- **Solution**: Ensure `htmlComponent` is passed to `setup()` function
- **Solution**: Check for typos in element IDs and function names

**Issue**: MATLAB errors not displayed to user
- **Solution**: Implement error event handling in both MATLAB and JavaScript
- **Solution**: Use try-catch in MATLAB and send error messages via `sendEventToHTMLSource`

**Issue**: Slow performance when sending data
- **Solution**: Reduce frequency of updates (throttle events)
- **Solution**: Send only changed data, not entire datasets
- **Solution**: Use appropriate data types (numbers vs strings)

**Issue**: Complex data structures not transferring correctly
- **Solution**: Use MATLAB structs (automatically converted to JSON)
- **Solution**: Avoid nested cell arrays; use struct arrays instead
- **Solution**: Test data transfer with simple examples first

**Issue**: Styling not appearing correctly
- **Solution**: Verify CSS is in `<style>` block inside `<head>`
- **Solution**: Check for CSS syntax errors
- **Solution**: Use browser dev tools to inspect computed styles

## Additional Resources

- MATLAB Documentation: `doc uihtml`
- HTML/CSS/JavaScript: MDN Web Docs
- Event handling: `doc sendEventToHTMLSource`
- Figure creation: `doc uifigure`
- Debugging: Use browser Developer Tools (F12)

## Related Skills

- **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.

More from matlab/agent-skills-playground

SkillDescription
agent-skill-authorUse this skill when the user wants to author, design, scope, or refine an Agent Skill (a SKILL.md file). Trigger phrases include "build a new skill", "design an agent skill", "scope a SKILL.md", "how should I structure this skill", "write a skill for X", "my skill isn't working well", or any request to improve an existing SKILL.md. Walks the user through an empirical, test-first process — probe the agent for real failures, design only for genuine knowledge gaps, iterate against runnable examples, and verify across models.
embedded-ai-deployment>
matlab-performance-optimizerOptimize MATLAB code for better performance through vectorization, memory management, and profiling. Use when user requests optimization, mentions slow code, performance issues, speed improvements, or asks to make code faster or more efficient.
matlab-projectUse this skill for any work involving a MATLAB Project (.prj file) — creating a new project, tracking files, managing the project path, configuring Simulink cache and code-generation folders, running project health checks, or writing build scripts that keep the project in sync with the file system. Trigger phrases include "set up a MATLAB project", "create a .prj", "track this file in the project", "project health check", "build script conventions". This skill is the generic foundation; domain-specific skills (e.g. `mbse-workflow`) build on it.
matlab-symbolic-mathGenerate correct MATLAB code using the Symbolic Math Toolbox. Use when the user asks for symbolic computations, analytical solutions, symbolic differentiation/integration, equation solving, or converting symbolic results to numeric MATLAB functions. Also use when converting differential equations to transfer functions or state-space form.
matlab-uihtml-designGenerate beautiful, distinctive HTML/CSS/JS control panels for MATLAB uihtml components. 8 built-in styles (Clean, Material, Cosmic Dark, Neumorphic, Dashboard Light, Midnight Gradient, Minimal Mono, Warm Dark) plus custom aesthetics. Produces production-grade UI with sliders, buttons, toggles, and panels. Use when building visually polished MATLAB app UIs with uihtml.
mbse-architectureUse this skill for the architecture phases of an MBSE workflow in MATLAB, when writing idempotent buildXxx.m scripts that produce a three-layer RFLPV architecture (Functional, Logical, Physical) with interface dictionaries, stereotype profiles, allocation sets, and requirements Implement links. Trigger for defining stereotype properties, functional-to-logical / logical-to-physical allocation, mapping requirements to components via slreq Implement links, or running quantitative roll-up analysis on the architecture. Do NOT trigger for ad-hoc structural edits to an already-built System Composer model (adding one component, rewiring a port) — use `building-simulink-models` with `model_edit` for that. Works alongside the `system-composer` skill for detailed SC API patterns.
mbse-workflowUse this skill for guided MBSE work in MATLAB — starting a new project, resuming work mid-workflow on an existing project, or answering orientation questions about how the MBSE skills fit together. Trigger when the user says they want to create, start, or set up a new MBSE project; work on a model-based systems engineering / RFLPV project; or asks which skill covers which phase. Walks through phases one at a time — propose → approve → generate → run → confirm. Use proactively whenever someone mentions starting or continuing an MBSE project.
simulink-requirementsUse this skill for all requirements-related work in a MATLAB MBSE project using the Requirements Toolbox (slreq). Covers creating and populating requirement sets, derivation links, test case requirements, verification coverage, reading and tracing links across requirement sets and models, checking link health, allocating requirements to components (Implement links), and building traceability reports. Trigger when the user asks about slreq API, slreqx files, slmx link files, outLinks/inLinks, traceability matrices, coverage analysis, broken links, or mapping requirements to architecture components. Use proactively for any requirements or traceability task.
system-composerUse this skill when authoring reusable, idempotent MATLAB scripts that build System Composer architecture models via the architecture-modeling API — `systemcomposer.createModel`, `addComponent`, `addPort`, `setInterface`, `connect(srcPort, dstPort)`, interface dictionaries (.sldd) with `addInterface`/`addElement`, profiles/stereotypes with `Profile.createProfile` and `addStereotype`, or `systemcomposer.allocation.createAllocationSet`. Also trigger when debugging these APIs (connections that don't appear, interfaces that don't resolve, profile save errors, `createAllocationSet` signature-mismatch errors). Do NOT trigger for ad-hoc structural edits to an already-built model (adding one SubSystem, rewiring a port) — use `building-simulink-models` with `model_edit` for that.