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:

Controls
Events
No events fired yet

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

  1. MATLAB class in a +ic package folder, extending Component or ComponentContainer
  2. Reactive properties with SetObservable, Description = "Reactive"
  3. Reactive events with Description = "Reactive"
  4. Reactive methods calling this.publish(name, data)
  5. Component map entry in component-map.ts
  6. Svelte component with $bindable() props, event functions, method props
  7. Defaults match between MATLAB and Svelte
  8. Build and check for warnings