Component Authoring
An IC component is three things: a MATLAB class that declares the properties, events and methods; an entry in the component map that links the MATLAB type to a Svelte file; and the Svelte component itself, which renders the UI and wires up the props.
We will build a PingBox from scratch: a colored square that pings and fires an event when you click it. By the end you will have a working component with a live playground on this page.
Step 1: The MATLAB class
Every component is a MATLAB class that extends ic.core.Component (or ic.core.ComponentContainer if it holds children). The class lives inside a +ic package folder so that MATLAB resolves it as ic.PingBox.
classdef PingBox < ic.core.Component Properties are marked with at least two attributes. SetObservable lets MATLAB detect changes via PostSet listeners. Description = "Reactive" tells the Reactive mixin to wire this property to the frontend automatically. Property names must be PascalCase in MATLAB (its important to follow the naming convention!). AbortSet is optional: it prevents sending a value to the frontend if it has not actually changed.
properties (SetObservable, AbortSet, Description = "Reactive")
Color (1,1) string = "#2563eb"
Size (1,1) string {mustBeMember(Size, ["sm","md","lg"])} = "md"
Disabled (1,1) logical = false
end Events only need Description = "Reactive". The Reactive mixin handles notifying them when the frontend requests so, so it’s important to leave the NotifyAccess public (or unset). Event names must also be PascalCase.
events (Description = "Reactive")
Pinged
end The constructor uses MATLAB’s name-value argument pattern (props.?ic.PingBox), which allows any property of the class to be passed as a name-value pair.
methods
function this = PingBox(props)
arguments
props.?ic.PingBox
props.ID (1,1) string = "ic-" + matlab.lang.internal.uuid()
end
this@ic.core.Component(props);
end
end Methods live in a separate methods (Description = "Reactive") block. Each method calls this.publish(name, data) with the same name as the MATLAB method, and any data that the frontend requires to execute it. publish returns the result to the request, which is a Promise on the MATLAB side.
methods (Description = "Reactive")
function out = ping(this)
out = this.publish("ping", []);
end
end All together, the full MATLAB class looks like this:
classdef PingBox < ic.core.Component
properties (SetObservable, AbortSet, Description = "Reactive")
Color (1,1) string = "#2563eb"
Size (1,1) string {mustBeMember(Size, ["sm","md","lg"])} = "md"
Disabled (1,1) logical = false
end
events (Description = "Reactive")
Pinged
end
methods
function this = PingBox(props)
arguments
props.?ic.PingBox
props.ID (1,1) string = "ic-" + matlab.lang.internal.uuid()
end
this@ic.core.Component(props);
end
end
methods (Description = "Reactive")
function out = ping(this)
out = this.publish("ping", []);
end
end
end Step 2: The Svelte component
Create the file at front/src/lib/components/misc/ping-box/PingBox.svelte. This is the view that renders in the browser and connects to the MATLAB class through props. It will use Svelte 5, so if you haven’t worked with Svelte before, check out the official tutorial to get familiar with the syntax.
Everything that is reactive comes into the Svelte component as props. The Reactive mixin wires up MATLAB properties, events, and methods to the frontend as props with specific conventions:
Reactive properties use $bindable() for every reactive property. This is what makes them two-way: when MATLAB sets Color, the Svelte component receives the new value; when Svelte writes back to a $bindable() prop, it publishes the change to MATLAB (after a 50ms debounce). Always try to provide a default value that matches the MATLAB default.
let {
color = $bindable('#2563eb'),
size = $bindable('md'),
disabled = $bindable(false),
pinged,
ping = $bindable((): Resolution => ({ success: true, data: null })),
}: {
color?: string;
size?: string;
disabled?: boolean;
pinged?: (data?: unknown) => void;
ping?: () => Resolution;
} = $props(); Reactive events are plain function props (not $bindable()), since they only flow one-way from Svelte to MATLAB. In our example, the pinged event is a callable function, or undefined if no MATLAB listener is attached (lazy activation). The Svelte component calls pinged?.({ count }) from a click handler. The ?. optional chaining is important: if MATLAB is not listening, the call is silently skipped and nothing crosses the bridge.
function handleClick() {
if (disabled) return;
count++;
doPing();
pinged?.({ count });
} Reactive methods must also be $bindable(), and be defined by the time the component mounts (use Svelte’s onMount() or $effect for this). If MATLAB calls a method before it’s defined, it will get the default no-op response ({ success: true, data: null }), so it’s important to set up the real implementation as soon as possible.
onMount(() => {
ping = (): Resolution => {
doPing();
return { success: true, data: null };
};
}); The rest is just Svelte code. The template binds the props to the UI and wires up event handlers. The styles define the look and animation of the PingBox.
<script lang="ts">
import type { Resolution } from '$lib/types';
import { onMount } from 'svelte';
let {
color = $bindable('#2563eb'),
size = $bindable('md'),
disabled = $bindable(false),
pinged,
ping = $bindable((): Resolution => ({ success: true, data: null })),
}: {
color?: string;
size?: string;
disabled?: boolean;
pinged?: (data?: unknown) => void;
ping?: () => Resolution;
} = $props();
const sizeMap: Record<string, string> = {
sm: '40px', md: '64px', lg: '96px'
};
let bouncing = $state(false);
let count = 0;
function doPing() {
bouncing = true;
setTimeout(() => { bouncing = false; }, 400);
}
function handleClick() {
if (disabled) return;
count++;
doPing();
pinged?.({ count });
}
onMount(() => {
ping = (): Resolution => {
doPing();
return { success: true, data: null };
};
});
</script>
<div
class="ic-ping-box"
class:ic-ping-box--ping={bouncing}
class:ic-ping-box--disabled={disabled}
style:width={sizeMap[size]}
style:height={sizeMap[size]}
style:background={color}
role="button"
tabindex="0"
onclick={handleClick}
></div>
<style>
.ic-ping-box {
border-radius: 3px;
cursor: pointer;
transition: opacity 0.15s;
}
.ic-ping-box--disabled {
opacity: 0.4;
cursor: not-allowed;
}
@keyframes ping {
0%, 100% { transform: scale(1); }
40% { transform: scale(0.85); }
60% { transform: scale(1.08); }
}
.ic-ping-box--ping {
animation: ping 0.4s ease;
}
</style> The wrapper div. The framework wraps every component in a <div style="display: contents"> automatically. This wrapper is invisible in the layout (it does not generate a box) but gives the framework a mount point and a place to attach the component ID. The display: contents behavior is relevant for components like flex children or table cells, where an extra DOM node would break the layout.
Step 3: The component map
The frontend needs to know which Svelte component corresponds to ic.PingBox. This mapping lives in component-map.ts, where each MATLAB type name points to a lazy-loaded Svelte module:
// front/src/lib/core/component-map.ts
const componentMap: Record<string, () => Promise<SvelteModule>> = {
// ... other components ...
'ic.PingBox': modules['../components/misc/ping-box/PingBox.svelte'],
}; The key is the fully qualified MATLAB class name (e.g. 'ic.PingBox'). The value is a dynamic import function that Vite resolves at build time. This is how the Bridge’s knows what module to dynamically import when it receives a message from MATLAB about an ic.PingBox.
Step 4: Try it
Here is the PingBox in action. Click the box and watch the event log:
And the MATLAB usage:
frame = ic.Frame("Parent", gl);
box = ic.PingBox("Color", "#22c55e", "Size", "lg");
frame.addChild(box);
addlistener(box, "Pinged", @(~, e) ...
disp("Ping #" + e.Data.count));
% Trigger the ping animation from MATLAB
box.ping(); Containers
If your component holds children, extend ic.core.ComponentContainer instead. The base class provides a Targets property that declares which named slots children can be inserted into.
Simple container
A container with a single default slot:
classdef MyContainer < ic.core.ComponentContainer
properties (SetObservable, AbortSet, Description = "Reactive")
Gap (1,1) double = 8
end
methods
function this = MyContainer(props)
arguments
props.?ic.MyContainer
props.ID (1,1) string = "ic-" + matlab.lang.internal.uuid()
end
this@ic.core.ComponentContainer(props);
% "default" target is already set by Container
end
end
end On the Svelte side, children arrive through the childEntries prop, keyed by target name. Render them with {@render child.snippet()}:
<script lang="ts">
import type { ChildEntries } from '$lib/types';
let {
gap = $bindable(8),
childEntries = {} as ChildEntries,
}: {
gap?: number;
childEntries?: ChildEntries;
} = $props();
</script>
<div class="ic-my-container" style:gap="{gap}px">
{#each childEntries.default ?? [] as child (child.id)}
{@render child.snippet()}
{/each}
</div>
<style>
.ic-my-container {
display: flex;
flex-wrap: wrap;
}
</style> The (child.id) keyed each block is important: it tells Svelte to match children by ID across re-renders, so that reordering children does not destroy and recreate DOM nodes.
Multiple targets
A container can declare multiple named targets. Children are inserted into specific slots via addChild(child, "targetName"):
classdef ToolBar < ic.core.ComponentContainer
methods
function this = ToolBar(props)
arguments
props.?ic.ToolBar
props.ID (1,1) string = "ic-" + matlab.lang.internal.uuid()
end
this@ic.core.ComponentContainer(props);
this.Targets = ["left", "center", "right"];
end
function validateChild(~, child, target)
assert(ismember(target, ["left","center","right"]), ...
"Target must be left, center, or right.");
end
end
end Override validateChild() to restrict which components go into which slots. The base Container already checks that the target name exists in Targets; your override adds component-type validation on top.
Checklist
- MATLAB class in a
+icpackage folder, extendingComponentorComponentContainer - Reactive properties with
SetObservable,Description = "Reactive" - Reactive events with
Description = "Reactive" - Reactive methods calling
this.publish(name, data) - Component map entry in
component-map.ts - Svelte component with
$bindable()props, event functions, method props - Defaults match between MATLAB and Svelte
- Build and check for warnings