NEXT HMIDocs
Widgets & styling / Building your own widgets

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

  1. 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 on window.__nextHMI__. Just reference the names.
  2. Read properties & data with SDK hooks — Resolve a schema field with usePropString / usePropNumber / usePropVar, subscribe to a tag with useVariable / useStructVariable, and write with sendWsMessage.
  3. Honour the editor's layout — Spread selfLayoutStyle(layout) on your outer element so the editor's basis/grow/min-size fields take effect, and widgetColorStyle(color) to let a colour field override the theme.
  4. Declare a schema — Export a schema so your fields show up in the properties panel, and exportedProperties so siblings can read your state via $widgetProp.
  5. Save — The compiler builds the module and pushes a widget_updated message 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 primitivesReact, useState, useEffect, useMemo, useCallback, useRef, createPortal.
  • Data & writesuseVariable, useStructVariable, useVariableMeta, useBindingValue, sendWsMessage, useEvalContext.
  • Property resolversusePropString / usePropNumber / usePropBoolean, usePropVar, usePropStruct, useRecordListProp, and the non-hook getProp* variants for use inside callbacks.
  • StylingselfLayoutStyle, widgetColorStyle, useCssVar, useHmiScope.
  • Cross-widget propsusePublishWidgetProp, so a sibling can read your state through $widgetProp.
  • Workspace datauseUsersData, useUserGroupsData, useLanguagesData.
  • NavigationuseNavigateToPage, usePageGroup, usePageTitle, useVisiblePages.
  • ActionsexecuteWidgetActions.
  • RecipesuseRecipeConfig, useRecipeState, recipeDownload, recipeUpload.
  • IconsgetBuiltinIconComponent, isBuiltinIconId.
  • ChartsRecharts (LineChart, XAxis, …) for custom trends and plots.
  • Virtual inputVirtualKeyboard and VirtualNumpad for touch panels, plus CloseButton.

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.