Homarr
ManagementCustom Widgets

Use the safe JSX runtime

Templates are JSX expressions interpreted inside a widget-local safety and render budget. They can read:

  • data.requestId — the latest response
  • status.requestId{ loading, ok, status, statusText, error }
  • options.name — saved widget configuration
  • inputs.name — temporary values from declarative bind

The v2 manifest requires a default source and supports up to eight sources, 64 requests, and 64 options. Source IDs are case-insensitively unique. Each request allows up to 32 static headers and 32 invalidation targets; a widget can contain up to 24 secrets, each up to 8,192 characters, and templates are limited to 50,000 characters. Validation reports the exact field when a limit is exceeded.

Authoring input accepts either template or templateLines, not both. templateLines accepts 1–2,000 lines, each up to 10,000 characters; after joining lines with newlines, the complete template must still fit the 50,000-character limit.

The safe interpreter also bounds JSX evaluation: AST depth 64, 25,000 operations, 4,000 collection items, 10,000 rendered nodes, and 200,000 characters in one string or rendered value. API responses are bounded separately before they reach the template.

Use normal Mantine layouts, typography, feedback, dates, and charts. Official compound names such as Card.Section, Tabs.List, and Radio.Group are supported. Ordinary safe props pass through; a dangerous capability produces an explicit error. Unsafe URLs, callback props, and unapproved style properties are stripped. Response data is deep-sanitized and budgeted before it reaches data, so treat it as untrusted input even when the source is private.

Temporary inputs

<Stack>
  <TextInput bind="search" defaultValue="" label="Search" />
  <Pagination bind="page" resetKey={inputs.search} defaultValue={1} total={5} />
  <SubFetch requestId="search" trigger="manual" params={{ query: inputs.search, page: inputs.page ?? 1 }}>
    {(results, meta) => (
      <Stack>
        {(results ?? []).map((item) => (
          <Text key={item.id}>{item.name}</Text>
        ))}
      </Stack>
    )}
  </SubFetch>
</Stack>

Bound values live only in React memory while the widget is mounted. They are not saved to the database, board options, exports, browser storage, or localStorage. Remounting resets them. Components can share a binding when their adapter types agree.

Bindable controls accept a scalar resetKey. When it changes, Homarr restores that control to its declared defaultValue or defaultChecked without running a request. This keeps dependent pagination correct while a manual SubFetch waits for the user to submit the new query.

Expression language

Use expressions, conditionals, optional chaining, arrays, objects, template strings, safe standard helpers, and expression callbacks for map, filter, sort, and trusted runtime slots. Callback blocks, authored const, IIFEs, authored recursion, arbitrary functions, imports, hooks, refs, raw events, browser requests, eval, and bigint are blocked.

For user-facing timestamps, use the safe static helpers instead of new Date or an invented component. Date.toLocaleString(value, "en-US", "UTC") produces a concise absolute date and time for an explicitly labeled UTC value. Date.toISOString, Date.toLocaleDateString, and Date.toLocaleTimeString are also available.

Bounded regex literals are supported for string matching, splitting, search, and replacement. Regex literals are limited to 128 characters and flags gimsu; lookarounds, backreferences, repeated alternatives/groups, unsupported flags, and more than one variable quantifier are rejected.

Runtime components

  • SubFetch runs a named query. Automatic queries run when the widget loads; set trigger="manual" to defer one and pass primitive params for its invocation references. With triggerContent, any content node (commonly a card or image) becomes the accessible click and keyboard trigger instead of showing the default load button.
  • SubFetch with trigger="manual" keeps its result local to that instance; it does not populate the shared data.requestId or status.requestId roots. The child callback receives (result, meta), while loading, errors, and retry are handled by SubFetch. Provide both triggerContent and triggerAriaLabel when custom content launches it.
  • SubFetch, ActionButton, and ToggleSwitch require a quoted literal requestId. Independent JSX validation rejects missing or computed IDs before a complete manifest or preview must be sent.
  • ActionButton and ToggleSwitch run actions only when the widget is mounted outside edit mode. A toggle needs both enabled and disabled request parameters and rolls back its visual state if the action fails.
  • RefreshButton renders a compact, accessible icon control that invalidates the client's load-query cache so active load queries can refetch. Set requestId inside a successful manual SubFetch result to rerun only that named query with the same parameters. A targeted rerun respects an explicit request cacheSeconds; an untargeted button also clears the widget's server cache. It is disabled when there is no active item/preview or while the board is in edit mode.
  • Icon/TablerIcon resolves installed safe icons.

Always include responsive loading, empty, error, and success states. Runtime failures remain inside the widget tile.

On this page