For the complete documentation index, see llms.txt. This page is also available as Markdown.

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

1

Open the Add tab

In the Settings panel, open Add.

2

Add Custom component

Under Basic components, drag Custom component to the preferred location on the page.

3

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.

4

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.

Until you enter an expression, you'll see the placeholder "Enter a Ninox script expression that returns HTML." The editor gives you AI assistance that already knows the Custom component syntax and constraints described here.

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-closing

The first letter of the tag name decides what it is:

Tag style
Meaning
Examples

lowercase

Native HTML element

div, span, p, button, input, a, img, ul, li, h1h6, 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, or status.

  • Reach related records, such as Contacts.email or line_items.

  • Use the full Ninox Script function library, such as select, sum, format, openRecord, alert, and icon.

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 / button

  • a keyboard event exposes key / code, plus a nested target (id, name, value, checked, tagName, etc.)

Live DOM nodes never cross into your script.

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 let you reassign during rendering, for example, a loop total, resets every render and stays an ordinary computed value.

  • State is per instance: a let inside a loop body or inside a Capitalized function component 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):

State vs. fields Reassigning a field status := "done" writes to the database. Reassigning a let only updates in‑memory UI state.

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

Attribute
Required
Description

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, row independently falls back to the surrounding scope, so you only specify what differs.

  • Once you override module or table, the current row no longer applies (it belongs to a different table), so you must give an explicit row:

  • row accepts 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.

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

  • Button

  • Icon

  • Card

  • CardHeader

  • CardBody

  • CardFooter

  • Modal

  • ModalContent

  • ModalHeader

  • ModalBody

  • ModalFooter

  • Popover

  • PopoverTrigger

  • PopoverContent

  • Progress

  • Tabs

  • Tab

  • Table

  • TableHeader

  • TableColumn

  • TableBody

  • TableRow

  • TableCell

  • Tooltip

  • Field

Button

Label goes in the children.

  • Common props: color, variant, size, radius

  • Boolean props: isDisabled, isLoading, isIconOnly, fullWidth

  • Prefer 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

Drive visibility with a boolean isOpen bound to a reactive let. Sizes range sm5xl/full. This example also embeds editable <Field>s:

Modal supports:

  • isOpen

  • onOpenChange

  • onClose

  • size

  • placement

  • backdrop

  • radius

  • scrollBehavior

  • Booleans:

    • isDismissable

    • isKeyboardDismissDisabled

    • hideCloseButton

    • shouldBlockScroll.

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 (type react[]) 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:

For genuinely custom values, use the style attribute instead:

What's available

Layout

  • block inline-block inline flex inline-flex grid hidden

  • flex-row/col/wrap

  • items-*

  • justify-*

  • self-*

  • grid-cols-1..6 (+12)

  • col-span-1..6/full

  • flex-1 grow shrink

Spacing

  • p/px/py/pt/pr/pb/pl

  • m… (incl. m*-auto)

  • gap/gap-x/gap-y

  • space-x/space-y on the scale 0, 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/screen and fractions w-1/2, w-1/3, w-2/3, w-1/4, etc.

  • max-w-xs..4xl/full

  • min-w-0

  • fixed heights h-4 … h-64

  • h-full/auto/fit

  • max-h-40/60/80/96/full

  • size-4/6/8/10/12

Typography

  • text-xs..4xl

  • font-thin..extrabold

  • text-left/center/right/justify

  • italic, underline, line-through, uppercase, lowercase, capitalize, truncate, whitespace-nowrap, break-words

  • leading-*

  • tracking-*

Colors

  • text-/bg-/border- in every standard Tailwind family (slaterose) at shades 50900, plus white, black, transparent, current.

  • Prefer the semantic theme tokens: default, primary, secondary, success, warning, danger, foreground, content1-4, divider, overlay (optionally -50..900 or -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/8

  • side borders border-solid/dashed/dotted rounded … rounded-full

Effects

  • shadow shadow-sm..2xl shadow-inner

  • ring ring-0/1/2/4

  • opacity-0/25/50/75/90/100.

Position and overflow

  • static, relative, absolute, fixed, sticky

  • inset-0

  • top/right/bottom/left-0

  • z-0..50

  • overflow-*

Backgrounds / fit / aspect

  • bg-cover/contain/center

  • object-cover/contain/fill

  • aspect-square/video

Interactivity and motion

  • cursor-*

  • select-*

  • pointer-events-*

  • transition, transition-colors, transition-transform

  • duration-100/150/200/300/500

  • ease-*

  • scale-95/100/105

  • rotate-45/90

Variant prefixes

Only these prefixes are generated:

  • hover:, focus:, dark: on color utilities

  • md:, 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 "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. Only http, https, mailto, and tel schemes are allowed. data: is permitted only on src for 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 react type (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 let state 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

Symptom
Likely cause / fix

"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.

The in‑editor AI assistant knows all of the syntax and constraints above, so you can also describe what you want and let it draft the expression for you.

Last updated

Was this helpful?