Custom component
Build custom, data-driven page interfaces with Ninox Script and JSX-like markup.
The Custom component lets you build custom UI on a page by writing a single Ninox Script expression that returns a piece of markup. You write JSX‑like tags directly in Ninox Script. Mix in your live record data, wire up buttons and events, and drop in ready‑styled building blocks. All without a separate widget file, bundler, or upload step.
If you already know how to write a Function field, you know most of this. A Custom component expression is evaluated the same way, scoped to the current record. It just returns UI instead of a number or text.
Add a Custom component to a page
Open the Add tab
In the Settings panel, open Add.
Add Custom component
Under Basic components, drag Custom component to the preferred location on the page.
Configure Custom component
In the component Settings, expand General. Under Logic, select to open the script editor. Enter the expression that returns your Custom component.
Review the result
Check whether the component shows the expected result. Then drag the component to move it on the page. Use the resize handles to adjust its size.
Write your first expression
Your expression must return the react type, a single root element built with tag syntax. Everything else about Ninox Script still works: let, function, if/else, for, switch, select, and the full function library.
<div class="p-4">
<h2 class="text-lg font-bold">Hello, {first_name}</h2>
<p class="text-default-500">Welcome back.</p>
</div>The result is rendered directly into the page, scoped to the current record. If the expression doesn't return the react type, you'll see: "The expression must return the react type. Use HTML tag syntax to build your UI."
Tags: HTML and components
Element syntax is JSX‑like:
<tag ...attributes>...children...</tag> // with children
<tag ... /> // self-closingThe first letter of the tag name decides what it is:
lowercase
Native HTML element
div, span, p, button, input, a, img, ul, li, h1–h6, table
Capitalized
A component
Your own function, or a built‑in like <Button>, <Card>, <Field>
Standard HTML attributes work on lowercase tags: class, id, style, href, src, type, placeholder, etc.
For Capitalized tags, name resolution is: your own function of that name wins, otherwise a built‑in component of that name is used, otherwise you get a "Function not found" warning. To avoid surprises, don't name your own functions after a built‑in tag.
Attributes: static and dynamic
Quoted string: a literal value
Curly braces: a Ninox expression evaluated at render time
Bare attribute: shorthand for ={true}
Plain text renders as‑is.
{ ... } embeds an expression value as text.
Build lists with a loop that returns an array of elements.
Using your record data
The expression runs scoped to the current record, exactly like a Function field:
Reference a field by name, such as
first_name,amount, orstatus.Reach related records, such as
Contacts.emailorline_items.Use the full Ninox Script function library, such as
select,sum,format,openRecord,alert, andicon.
Use { field_name } when you only need the value as text. Use the <Field> tag when you want the real, editable widget.
Event handlers
Any attribute whose name starts with on, for example, onClick, onChange, or onPress, is an event handler. It runs when the event fires, not at render time. It must be a function.
Handlers may perform side effects, for example, show a message, open a record, write to a field, or update reactive state.
To read data about the event, take a parameter.
The event you receive is a safe snapshot, a plain object with only the relevant primitive fields, for example:
a mouse event exposes
clientX/buttona keyboard event exposes
key/code, plus a nestedtarget(id,name,value,checked,tagName, etc.)
Live DOM nodes never cross into your script.
For HeroUI components like <Button>, prefer onPress over onClick. It behaves consistently across mouse, touch, and keyboard.
Reactive state with let
A let inside a Custom component expression is a reactive component state. Think of it like a small piece of UI memory:
Its initializer runs once to seed the value (like a default). Later renders reuse the stored value.
Reassigning it (
:=) from inside an event handler stores the new value and re‑renders the component.Only reassignments made inside a handler persist. A
letyou reassign during rendering, for example, a loop total, resets every render and stays an ordinary computed value.State is per instance: a
letinside a loop body or inside a Capitalizedfunctioncomponent gets its own independent value each time.State resets when the record changes or when you edit the expression.
Counter example:
Per‑instance state inside a component used in a loop:
A render‑time computation (not state, recomputed every render):
The <Field> tag
<Field> embeds an actual database field with its native Ninox editor. It can render fields such as a text input, date picker, choice dropdown, file upload, or reference picker. It is not just a formatted value. The tag is self‑closing.
Attributes
field
✅
The field's script name.
module
Module name. Defaults to the current scope.
table
Table name. Defaults to the current scope.
row
Row id (number). Defaults to the current scope's row.
readonly
Renders the field display‑only.
How targeting works
By default the field binds to the current module, table, and row.
Each of
module,table,rowindependently falls back to the surrounding scope, so you only specify what differs.Once you override
moduleortable, the current row no longer applies (it belongs to a different table), so you must give an explicitrow:rowaccepts a number (row={1}), a numeric string (row="1"), or an expression (row={this.id},row={item.id}).Make a field display‑only with
readonly:
Edits made in a <Field> write back to the target record (unless readonly).
Reference and reverse‑reference fields
<Field> fully supports reference and reverse‑reference fields. The picker renders just like it does elsewhere in Ninox. For these types the embedded picker shows the referenced table's first 5 columns.
Rule of thumb: Use <Field field="…" /> for the real interactive widget; use { field_name } when you only need the value as text.
Built‑in components (HeroUI)
Capitalized tags for these ready‑styled components are available out of the box. They always follow the active light/dark theme.
Available components
ButtonIconCardCardHeaderCardBodyCardFooterModalModalContentModalHeader
ModalBodyModalFooterPopoverPopoverTriggerPopoverContentProgressTabsTab
TableTableHeaderTableColumnTableBodyTableRowTableCellTooltipField
Button
Label goes in the children.
Common props:
color,variant,size,radiusBoolean props:
isDisabled,isLoading,isIconOnly,fullWidthPrefer
onPress
Card
Card supports shadow, radius, fullWidth, isHoverable, isPressable.
Tabs
Each <Tab> needs a unique key and a title.
Table
Cell count must match the columns; give each row a unique key.
Table supports isStriped, isCompact, hideHeader, removeWrapper, selectionMode.
Progress
Tooltip
content plus a single trigger child; props placement, color, delay, closeDelay.
Popover
Modal
Drive visibility with a boolean isOpen bound to a reactive let. Sizes range sm…5xl/full. This example also embeds editable <Field>s:
Modal supports:
isOpenonOpenChangeonClosesizeplacementbackdropradiusscrollBehaviorBooleans:
isDismissableisKeyboardDismissDisabledhideCloseButtonshouldBlockScroll.
Custom components with function
Define reusable pieces with a Capitalized function:
Attributes bind to parameters by name (not by position). Unmatched parameters receive
null.A parameter named
children(typereact[]) receives the nested child nodes:
Compose your UI from small, reusable components.
Styling with Tailwind classes
Style with Tailwind utility classes in the class attribute. There's one important rule:
Only a fixed, curated set of Tailwind utilities is available. Because your class strings are computed at runtime, Tailwind can't scan them at build time.
Any class outside the supported set silently has no effect, and arbitrary‑value classes never work, anything with square brackets like w-[473px], bg-[#1e90ff], or [&>div]:….
For genuinely custom values, use the style attribute instead:
What's available
Layout
block inline-block inline flex inline-flex grid hiddenflex-row/col/wrapitems-*justify-*self-*grid-cols-1..6(+12)col-span-1..6/fullflex-1 grow shrink
Spacing
p/px/py/pt/pr/pb/plm…(incl.m*-auto)gap/gap-x/gap-yspace-x/space-yon the scale0, 0.5, 1, 1.5, 2, 2.5, 3, 3.5, 4, 5, 6, 7, 8, 10, 12, 14, 16, 20, 24
Sizing
w-full/auto/fit/screenand fractionsw-1/2,w-1/3,w-2/3,w-1/4, etc.max-w-xs..4xl/fullmin-w-0fixed heights
h-4 … h-64h-full/auto/fitmax-h-40/60/80/96/fullsize-4/6/8/10/12
There are no root‑height utilities like h-screen. Don't try to set the component's overall height.
Typography
text-xs..4xlfont-thin..extraboldtext-left/center/right/justifyitalic,underline,line-through,uppercase,lowercase,capitalize,truncate,whitespace-nowrap,break-wordsleading-*tracking-*
Colors
text-/bg-/border-in every standard Tailwind family (slate…rose) at shades50–900, pluswhite,black,transparent,current.Prefer the semantic theme tokens:
default,primary,secondary,success,warning,danger,foreground,content1-4,divider,overlay(optionally-50..900or-foreground), for example,bg-primary,text-foreground,bg-content1,text-default-500,border-divider. These adapt to light/dark automatically.
Borders and radius
border border-0/2/4/8side borders
border-solid/dashed/dottedrounded … rounded-full
Effects
shadow shadow-sm..2xl shadow-innerring ring-0/1/2/4opacity-0/25/50/75/90/100.
Position and overflow
static,relative,absolute,fixed,stickyinset-0top/right/bottom/left-0z-0..50overflow-*
Backgrounds / fit / aspect
bg-cover/contain/centerobject-cover/contain/fillaspect-square/video
Interactivity and motion
cursor-*select-*pointer-events-*transition,transition-colors,transition-transformduration-100/150/200/300/500ease-*scale-95/100/105rotate-45/90
Variant prefixes
Only these prefixes are generated:
hover:,focus:,dark:on color utilitiesmd:,lg:on layout, spacing, sizing, and text utilities
Not available: active:, disabled:, group-hover:, sm:, xl:, 2xl:. For states like active/disabled, drive them with an event handler plus a reactive let, or compute the class string yourself:
The built‑in HeroUI components are always fully styled regardless of the safelist, so reaching for <Card>, <Button>, etc. is the easiest way to get a polished look.
The "Use shadow DOM" option
Each Custom component has a Use shadow DOM toggle (default: off).
When enabled, the component renders inside a shadow DOM, which isolates its styles. The component's CSS can't affect the rest of the page, and the page's CSS can't leak in. Enable it when a component's styling should be fully self‑contained.
Notes:
Tailwind and HeroUI classes still resolve inside the shadow DOM. The theme stylesheet is loaded into the shadow root for you.
Because that stylesheet loads asynchronously, content may flash briefly unstyled on first render.
Interactivity (events, HeroUI popovers, focus handling) works across the shadow boundary. There are no functional differences.
Leave it off unless you specifically need style isolation.
Security and limitations
How it's kept safe
Your expression produces a structured element tree, not an HTML string. There's no raw HTML/JS injection surface.
Certain tags are blocked (dropped):
script,iframe,object,embed,link,meta,base,body,head,html,title. (<style>is allowed and is scoped when shadow DOM is on.)URLs on
href,src,action, and similar attributes are screened. Onlyhttp,https,mailto, andtelschemes are allowed.data:is permitted only onsrcfor inline images.Event handlers are Ninox Script only, there's no execution of JavaScript strings. Handlers run with your normal script permissions.
This is content isolation for a trusted workspace builder, meaning the author of the expression. (For running fully untrusted third‑party code, use Custom Widgets, which run in a sandboxed iframe.)
Things to keep in mind
The expression must return the
reacttype (a single root element) or nothing renders.Only the curated Tailwind vocabulary works; no arbitrary values; limited variant prefixes.
Don't set the component's overall height on the root element, the page manages sizing.
Embedded reference / reverse
<Field>pickers show the first 5 columns of the referenced table.Reactive
letstate resets when the record changes or the expression is edited; only handler‑phase reassignments persist.If one of your components throws an error, it is contained to that component. It will not tear down the whole page.
Troubleshooting
"The expression must return the react type."
Your expression doesn't end with a tag. Make the last value an element.
A CSS class does nothing
It's outside the supported vocabulary or uses an arbitrary value ([...]). Use a supported class or the style attribute.
Nothing happens on click
The handler must be a function: onClick={function () do … end}, not onClick={doThing()}.
A counter/toggle resets instantly
You reassigned the let during render instead of inside a handler; only handler reassignments persist.
"Function not found" for a Capitalized tag
It's neither a defined function nor a built‑in component. Check spelling/casing.
<Field> error: "has no row in scope"
You set module/table, so you must also supply an explicit row.
<Field> error: "can only be used inside a Custom component"
<Field> only works within a Custom component expression.
A tag is missing from the output
It may be a blocked tag (script, iframe, …) or a URL with a disallowed scheme.
Last updated
Was this helpful?