Building your own widgets
When the catalog stops, TypeScript starts. Drop a .tsx file into your project's custom-widgets/ folder and the backend compiles it on save and hot-swaps it into the running app — no core rebuild, no Node toolchain, no page reload.
The folder contract
Each widget is a folder under the project's custom-widgets/:
| File | Role |
|---|---|
<Name>/index.tsx |
Your source. Exports the default component, and optionally schema and exportedProperties. |
<runtime-home>/.widget-build/…/index.js |
Generated runtime cache — never edit or copy into the project. |
<Name>/style.css |
Optional stylesheet, injected on mount, removed on unmount. |
<Group>/<Name>/… |
Nest a folder to group widgets in the Add-widget menu. |
Folders starting with . or _ are ignored — keep a scaffold in _template/ without it being picked up.
Write one
- Create the file — Add
custom-widgets/SpeedTile/index.tsx. Do not import React or app modules — every hook and helper is handed to you as an SDK global onwindow.__nextHMI__. Just reference the names. - Read properties & data with SDK hooks — Resolve a schema field with
usePropString/usePropNumber/usePropVar, subscribe to a tag withuseVariable/useStructVariable, and write withsendWsMessage. - Honour the editor's layout — Spread
selfLayoutStyle(layout)on your outer element so the editor's basis/grow/min-size fields take effect, andwidgetColorStyle(color)to let a colour field override the theme. - Declare a schema — Export a
schemaso your fields show up in the properties panel, andexportedPropertiesso siblings can read your state via$widgetProp. - Save — The compiler builds the module and pushes a
widget_updatedmessage over the WebSocket; the editor re-imports it live.
// no imports — SDK names come from window.__nextHMI__
export default function SpeedTile({ properties, layout }: HmiWidgetProps) {
const label = usePropString(properties, 'label', 'Speed')
const rpm = useVariable('LinePLC:Motor1/Speed')
return (
<div className="speed-tile" style={selfLayoutStyle(layout)}>
<span className="lbl">{label}</span>
<strong>{rpm ?? '—'}</strong>
</div>
)
}
// fields shown in the editor's properties panel
export const schema = { label: { type: 'string', label: 'Label' } }
What the SDK gives you
Every name below is a global — no import, ever.
- React primitives —
React,useState,useEffect,useMemo,useCallback,useRef,createPortal. - Data & writes —
useVariable,useStructVariable,useVariableMeta,useBindingValue,sendWsMessage,useEvalContext. - Property resolvers —
usePropString/usePropNumber/usePropBoolean,usePropVar,usePropStruct,useRecordListProp, and the non-hookgetProp*variants for use inside callbacks. - Styling —
selfLayoutStyle,widgetColorStyle,useCssVar,useHmiScope. - Cross-widget props —
usePublishWidgetProp, so a sibling can read your state through$widgetProp. - Workspace data —
useUsersData,useUserGroupsData,useLanguagesData. - Navigation —
useNavigateToPage,usePageGroup,usePageTitle,useVisiblePages. - Actions —
executeWidgetActions. - Recipes —
useRecipeConfig,useRecipeState,recipeDownload,recipeUpload. - Icons —
getBuiltinIconComponent,isBuiltinIconId. - Charts —
Recharts(LineChart,XAxis, …) for custom trends and plots. - Virtual input —
VirtualKeyboardandVirtualNumpadfor touch panels, plusCloseButton.
Reach for a third-party library
Drop an ESM bundle into the project's external-libraries/ folder and import it by name through the generated import map — again, no core rebuild. Naming convention and the override file are in Files & assets.
Type declarations for the whole SDK live in custom-widgets-sdk.d.ts at the repo root. Reference it from your project's tsconfig and you get full editor completion for every global. The SDK carries its own version number, bumped only when a name is removed or a signature changes — additions don't move it.
The full contract — schema fields, styling tokens, struct and array binding shapes, action triggering, and testing — is in the custom-widget reference.