Create Custom Pluggable Widget

SkillDev tools

Lets your agent create a Claude skill that builds custom Mendix widgets with React and TypeScript.

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the Create Custom Pluggable Widget skill

About this capability

Build a Mendix pluggable widget from scratch with React and TypeScript and package it as an .mpk. Use when no marketplace or built-in widget covers what is needed and a custom React component has to be written.

What this skill tells your AI

The instructions your AI receives, as published by mendixlabs/mxcli in .claude/skills/mendix/create-custom-widget/SKILL.md and read by ahel’s review.

Build a Mendix pluggable widget from scratch using React + TypeScript. Produces a .mpk file ready for Studio Pro.

Prerequisites

  • Node.js >= 16
  • npm

Step 1: Scaffold the Project

Create a directory and generate all source files. Use PascalCase for the widget name.

mkdir -p <WidgetName>/src/components <WidgetName>/src/ui

package.json

{
  "name": "<widget-name>",
  "widgetName": "<WidgetName>",
  "version": "1.0.0",
  "description": "<description>",
  "license": "Apache-2.0",
  "config": {
    "projectPath": "./tests/testProject",
    "mendixHost": "http://localhost:8080",
    "developmentPort": 3000
  },
  "packagePath": "com.example.widgets",
  "scripts": {
    "dev": "pluggable-widgets-tools start:web",
    "build": "pluggable-widgets-tools build:web",
    "lint": "pluggable-widgets-tools lint",
    "lint:fix": "pluggable-widgets-tools lint:fix"
  },
  "devDependencies": {
    "@mendix/pluggable-widgets-tools": "^11.6.0",
    "@types/big.js": "^6.0.2"
  },
  "dependencies": {
    "classnames": "^2.2.6"
  },
  "resolutions": {
    "react": "^19.0.0",
    "react-dom": "^19.0.0",
    "@types/react": "^19.0.0",
    "@types/react-dom": "^19.0.0"
  },
  "overrides": {
    "react": "^19.0.0",
    "react-dom": "^19.0.0",
    "@types/react": "^19.0.0",
    "@types/react-dom": "^19.0.0"
  }
}

Naming rules:

  • name: kebab-case (npm package name)
  • widgetName: PascalCase (matches .xml and .tsx filename)
  • packagePath: reverse domain, dot-separated (e.g. com.example.widgets)

tsconfig.json

{
  "extends": "./node_modules/@mendix/pluggable-widgets-tools/configs/tsconfig.base.json"
}

src/package.xml

<?xml version="1.0" encoding="utf-8" ?>
<package xmlns="http://www.mendix.com/package/1.0/">
    <clientModule name="<WidgetName>" version="1.0.0" xmlns="http://www.mendix.com/clientModule/1.0/">
        <widgetFiles>
            <widgetFile path="<WidgetName>.xml"/>
        </widgetFiles>
        <files>
            <file path="com/example/widgets/<widgetname>"/>
        </files>
    </clientModule>
</package>

The <file path> must match packagePath + lowercase widget name, with dots replaced by /. For example, for HelloWorld with packagePath=com.example.widgets, the path is com/example/widgets/helloworld.

Step 2: Define Widget Properties (widget.xml)

src/<WidgetName>.xml

<?xml version="1.0" encoding="utf-8"?>
<widget id="com.example.widgets.<widgetname>.<WidgetName>"
        pluginWidget="true"
        needsEntityContext="true"
        offlineCapable="true"
        supportedPlatform="Web"
        xmlns="http://www.mendix.com/widget/1.0/"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://www.mendix.com/widget/1.0/ ../node_modules/mendix/custom_widget.xsd">
    <name><widget Name></name>
    <description><description></description>
    <icon/>
    <properties>
        <propertyGroup caption="General">
            <!-- Add properties here -->
        </propertyGroup>
    </properties>
</widget>

The id attribute must be <packagePath>.<widgetname>.<WidgetName> — the second-to-last segment is the lowercase widget name, which becomes the JS subdirectory. This must match the <file path> in package.xml.

Set needsEntityContext="true" when the widget needs entity data. Set to "false" for standalone widgets.

Property Type Reference

XML TypeMendix TypeUse CaseExample
stringStatic textLabels, titles<property key="title" type="string"><caption>title</caption></property>
booleanToggleShow/hide, enable<property key="showHeader" type="boolean" defaultValue="true"><caption>show header</caption></property>
integerNumberCounts, sizes<property key="columns" type="integer" defaultValue="3"><caption>columns</caption></property>
decimalDecimalMeasurements<property key="opacity" type="decimal" defaultValue="1.0"><caption>Opacity</caption></property>
enumerationEnum choiceMode selectionSee below
expressionDynamic valueComputed text<property key="label" type="expression" defaultValue=""><caption>label</caption><returnType type="string"/></property>
textTemplateTemplate textFormatted text with paramsSee below
attributeEntity attributeData bindingSee below
datasourceList data sourceLists, grids<property key="datasource" type="datasource" isList="true"><caption>data source</caption></property>
widgetsChild widgetsContent slots<property key="content" type="widgets" required="false"><caption>content</caption></property>
actionOn-click actionButtons, links<property key="onclick" type="action"><caption>on click</caption></property>
iconIconDecorative<property key="icon" type="icon" required="false"><caption>icon</caption></property>
imageImageAvatar, logo<property key="image" type="image" required="false"><caption>image</caption></property>
objectCompoundComplex configSee below

Enumeration Example

<property key="alignment" type="enumeration" defaultValue="center">
    <caption>Alignment</caption>
    <description/>
    <enumerationValues>
        <enumerationValue key="left">left</enumerationValue>
        <enumerationValue key="center">Center</enumerationValue>
        <enumerationValue key="right">right</enumerationValue>
    </enumerationValues>
</property>

Attribute Binding Example

<property key="value" type="attribute">
    <caption>value</caption>
    <description>The attribute to display</description>
    <attributeTypes>
        <attributeType name="string"/>
        <attributeType name="integer"/>
        <attributeType name="decimal"/>
    </attributeTypes>
</property>

TextTemplate Example

<property key="displayText" type="textTemplate">
    <caption>display text</caption>
    <description/>
    <translations>
        <translation lang="en_US">default text</translation>
    </translations>
</property>

Object (Compound) Example — e.g. column definitions

<property key="columns" type="object" isList="true">
    <caption>columns</caption>
    <description/>
    <properties>
        <propertyGroup caption="column">
            <property key="header" type="textTemplate">
                <caption>header</caption>
                <translations><translation lang="en_US">column</translation></translations>
            </property>
            <property key="attribute" type="attribute" datasource="datasource">
                <caption>attribute</caption>
                <attributeTypes>
                    <attributeType name="string"/>
                    <attributeType name="integer"/>
                </attributeTypes>
            </property>
            <property key="width" type="integer" defaultValue="100">
                <caption>width (px)</caption>
            </property>
        </propertyGroup>
    </properties>
</property>

Note: datasource="datasource" links the attribute picker to the datasource property.

Property Groups

Use nested <propertyGroup> for Studio Pro tab organization:

<properties>
    <propertyGroup caption="General">
        <!-- main properties -->
    </propertyGroup>
    <propertyGroup caption="Appearance">
        <!-- style properties -->
    </propertyGroup>
    <propertyGroup caption="events">
        <!-- action properties -->
    </propertyGroup>
</properties>

Step 3: Write the Entry Component

src/<WidgetName>.tsx

import { ReactElement } from "react";
import { <WidgetName>ContainerProps } from "../typings/<WidgetName>Props";
import { MyComponent } from "./components/MyComponent";
import "./ui/<WidgetName>.css";

export function <WidgetName>(props: <WidgetName>ContainerProps): ReactElement {
    // map Mendix props to React component props
    return <MyComponent {...relevantProps} />;
}

The typings/<WidgetName>Props.d.ts file is auto-generated by the build tool from the .xml definition. Do NOT create it manually.

Key Mendix Prop Patterns

// string property
props.title  // string

// boolean property
props.showHeader  // boolean

// expression property
props.label?.value  // string | undefined (use .value to get resolved text)

// attribute property (read)
props.value?.displayValue  // string
props.value?.value  // actual typed value

// attribute property (write)
props.value?.setValue(newValue)

// TextTemplate property
props.displayText?.value  // string (resolved template)

// action property
props.onClick?.canExecute  // boolean
props.onClick?.execute()   // trigger the action

// datasource property
props.dataSource?.items  // ObjectItem[] | undefined
props.dataSource?.status  // "available" | "loading"

// widgets property (content slot)
props.content  // ReactNode

// icon property
import { icon } from "mendix/components/web/icon";
<icon icon={props.icon} />

// object list property (e.g. columns)
props.columns  // Array<{ header, attribute, width }>
// access attribute value for a specific item:
props.columns[0].attribute?.get(item)?.displayValue

Step 4: Write the React Component

src/components/MyComponent.tsx

Keep the component pure React — no Mendix API dependencies. This makes it testable and reusable.

import { ReactElement } from "react";
import classNames from "classnames";

export interface MyComponentProps {
    title: string;
    value?: string;
    className?: string;
}

export function MyComponent({ title, value, className }: MyComponentProps): ReactElement {
    return (
        <div className={classNames("widget-my-component", className)}>
            <h3>{title}</h3>
            {value && <p>{value}</p>}
        </div>
    );
}

Step 5: Editor Config (optional but recommended)

src/<WidgetName>.editorConfig.ts

Controls how the widget appears in Studio Pro's design mode:

import { <WidgetName>PreviewProps } from "../typings/<WidgetName>Props";

export type properties = PropertyGroup[];
type PropertyGroup = {
    caption: string;
    propertyGroups?: PropertyGroup[];
    properties?: Property[];
};
type Property = {
    key: string;
    caption: string;
    description?: string;
};

export function getProperties(
    _values: <WidgetName>PreviewProps,
    defaultProperties: properties
): properties {
    return defaultProperties;
}

Step 6: CSS Styles

src/ui/<WidgetName>.css

.widget-<widget-name> {
    /* widget styles */
}

Use a .widget-<widget-name> prefix to avoid CSS collisions.

Step 7: Build

cd <widget-dir>
npm install
npm run build

Output: dist/<version>/com.example.widgets.<WidgetName>.mpk

Step 8: Install to Mendix Project

cp dist/*/*.mpk /path/to/mendix-project/widgets/

Then open/reload the project in Studio Pro.

Common Widget Patterns

KPI Card

Properties: title (string), value (expression/string), icon (icon), trend (enumeration: up/down/neutral), onclick (action)

Chart Wrapper

Properties: datasource (datasource), valueAttr (attribute/decimal), labelAttr (attribute/string), chartType (enumeration), height (integer)

Wrap a charting library (Chart.js, Recharts) inside the component.

Custom Input

Properties: value (attribute/string, writable), placeholder (string), onchange (action), validation (expression/string)

Set needsEntityContext="true". Use props.value.setValue() for two-way binding.

Layout Component

Properties: content (widgets), columns (integer), gap (integer)

Set needsEntityContext="false". Render children via {props.content}.

Checklist Before Build

  • id in .xml matches packagePath.WidgetName
  • <name> in package.xml matches .xml filename (without extension)
  • <file path> in package.xml matches packagePath with / separators
  • Entry .tsx exports a function with the exact widget name
  • CSS file imported in entry .tsx
  • needsEntityContext matches whether entity data is needed
  • No manual Props.d.ts file (auto-generated by build tool)
  • All expression properties have <returnType>
  • All attribute properties list valid <attributeType> entries
  • object properties with attributes set datasource reference

Troubleshooting

ErrorCauseFix
Cannot find module '../typings/...'Haven't built yetRun npm run build first, types are generated
widget not showing in Studio ProWrong id in XMLEnsure id="packagePath.WidgetName"
CE0463 widget definition changedProperty mismatchEnsure XML and component props match
pluginWidget must be trueMissing attributeAdd pluginWidget="true" to <widget>

Signals

GitHub stars
122
Forks
49
Last commit
Sep 2026
Advanced
Catalog kind
skill
Gateway key
create-custom-widget
Source
github.com/mendixlabs/mxcli