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 responsestatus.requestId—{ loading, ok, status, statusText, error }options.name— saved widget configurationinputs.name— temporary values from declarativebind
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
SubFetchruns a named query. Automatic queries run when the widget loads; settrigger="manual"to defer one and pass primitiveparamsfor its invocation references. WithtriggerContent, any content node (commonly a card or image) becomes the accessible click and keyboard trigger instead of showing the default load button.SubFetchwithtrigger="manual"keeps its result local to that instance; it does not populate the shareddata.requestIdorstatus.requestIdroots. The child callback receives(result, meta), while loading, errors, and retry are handled bySubFetch. Provide bothtriggerContentandtriggerAriaLabelwhen custom content launches it.SubFetch,ActionButton, andToggleSwitchrequire a quoted literalrequestId. Independent JSX validation rejects missing or computed IDs before a complete manifest or preview must be sent.ActionButtonandToggleSwitchrun 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.RefreshButtonrenders a compact, accessible icon control that invalidates the client's load-query cache so active load queries can refetch. SetrequestIdinside a successful manualSubFetchresult to rerun only that named query with the same parameters. A targeted rerun respects an explicit requestcacheSeconds; 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/TablerIconresolves installed safe icons.
Always include responsive loading, empty, error, and success states. Runtime failures remain inside the widget tile.