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.

Add ahel to your AI once: Claude, ChatGPT, Cursor, Claude Code or Codex. Then ask it to use this.

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

About this skill

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

ahel review

  • K1binfo
    installs-packages

Automated review, not a security audit. Ruleset v1+k2.

Advanced
Catalog kind
skill
Key
create-custom-widget
Source
github.com/mendixlabs/mxcli