create-custom-widget · git:20260821.ff81a24 · 2026-08-21 · sha256 be0a0b5fc2e333a4
create-custom-widget git:20260821.ff81a24A
Immutable. This exact content is served forever at /api/v1/blob/be0a0b5fc2e333a4.
---
name: create-custom-widget
description: "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."
---
# Create Custom Pluggable Widget
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.
```bash
mkdir -p <WidgetName>/src/components <WidgetName>/src/ui
```
### package.json
```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
```json
{
"extends": "./node_modules/@mendix/pluggable-widgets-tools/configs/tsconfig.base.json"
}
```
### src/package.xml
```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
<?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 Type | Mendix Type | Use Case | Example |
|----------|------------|----------|---------|
| `string` | Static text | Labels, titles | `<property key="title" type="string"><caption>title</caption></property>` |
| `boolean` | Toggle | Show/hide, enable | `<property key="showHeader" type="boolean" defaultValue="true"><caption>show header</caption></property>` |
| `integer` | Number | Counts, sizes | `<property key="columns" type="integer" defaultValue="3"><caption>columns</caption></property>` |
| `decimal` | Decimal | Measurements | `<property key="opacity" type="decimal" defaultValue="1.0"><caption>Opacity</caption></property>` |
| `enumeration` | Enum choice | Mode selection | See below |
| `expression` | Dynamic value | Computed text | `<property key="label" type="expression" defaultValue=""><caption>label</caption><returnType type="string"/></property>` |
| `textTemplate` | Template text | Formatted text with params | See below |
| `attribute` | Entity attribute | Data binding | See below |
| `datasource` | List data source | Lists, grids | `<property key="datasource" type="datasource" isList="true"><caption>data source</caption></property>` |
| `widgets` | Child widgets | Content slots | `<property key="content" type="widgets" required="false"><caption>content</caption></property>` |
| `action` | On-click action | Buttons, links | `<property key="onclick" type="action"><caption>on click</caption></property>` |
| `icon` | Icon | Decorative | `<property key="icon" type="icon" required="false"><caption>icon</caption></property>` |
| `image` | Image | Avatar, logo | `<property key="image" type="image" required="false"><caption>image</caption></property>` |
| `object` | Compound | Complex config | See below |
### Enumeration Example
```xml
<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
```xml
<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
```xml
<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
```xml
<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:
```xml
<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
```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
```tsx
// 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.
```tsx
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:
```ts
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
```css
.widget-<widget-name> {
/* widget styles */
}
```
Use a `.widget-<widget-name>` prefix to avoid CSS collisions.
## Step 7: Build
```bash
cd <widget-dir>
npm install
npm run build
```
Output: `dist/<version>/com.example.widgets.<WidgetName>.mpk`
## Step 8: Install to Mendix Project
```bash
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
| Error | Cause | Fix |
|-------|-------|-----|
| `Cannot find module '../typings/...'` | Haven't built yet | Run `npm run build` first, types are generated |
| `widget not showing in Studio Pro` | Wrong `id` in XML | Ensure `id="packagePath.WidgetName"` |
| `CE0463 widget definition changed` | Property mismatch | Ensure XML and component props match |
| `pluginWidget must be true` | Missing attribute | Add `pluginWidget="true"` to `<widget>` |