AI: Good practices for building views
The other AI pages say what is valid. This page says what is good: the choices a careful designer makes so the view looks finished the first time the person sees it, and does not need a second round of fixes. Every rule here comes from a view that looked wrong or behaved wrong until it was followed.
Read it before the first change. Over MCP, nvb_execute does not accept a batch until this page and the other required pages were read on the connection.
How to work
- Look before you build. Call
getTree()(MCP:nvb_get_tree) first. Extend what is there; never rebuild a view the user only asked to change. - One request, one batch. Send everything a request needs in one
execute()call. It is atomic, so a failure leaves nothing half built, and the person can undo it in one step. - Dry run large batches with
dryRun: true, fix what it reports, then send it for real. - Check the result. After a batch, read the returned tree (or
getTree()) and confirm the elements are where you meant them: in the right container, in the right order, side by side where you wanted a row. - Do not save. Saving is the person's decision. Build, check, then stop and describe what you did.
When to ask, and when to decide
When you work in a live builder, the person is right there. A short question costs them a few seconds; a view built on a wrong guess costs them a round of fixes. So when you are genuinely unsure about something that changes what they get, ask before you build it.
Ask when the choice changes how the view looks or behaves and the request does not settle it:
- Which control for a choice: a
radiolist with every option visible, a compactselect, aselectButtonrow, orautocompletefor a long list; one answer or several (checkbox,multiSelect). - Which kind of table: rows people edit (
dynamicTable), data shown from a server with paging and search (table), or a repeating block of fields (dynamicPanel). - One page or steps: a single form with sections, or a multi-step flow.
- What is required, and what happens on submit (a toast, a data source call, navigation).
- Where the data comes from: fixed options, or a data source, and which one.
- Anything you would have to invent: option lists, labels in another language, business rules, limits.
Decide yourself, and say what you chose, when a good practice already answers it or the change is easy to undo: labels in plain words, side-by-side short fields, button tones, required messages, names, layout details. Asking about every detail is as unhelpful as guessing the important ones.
How to ask:
- One short message, before the batch that depends on it, with every open question together, not one at a time.
- Offer concrete options and your recommendation: "For Client type I would use a radio list, since there are only two options and both stay visible. Or should it be a dropdown?"
- Build everything that does not depend on the answer while you wait, if the person agrees, and leave the open parts out rather than filling them with a guess.
- Once the person has answered, follow the answer, including in later requests of the same conversation.
Captions and labels
Every element sits in a wrapper that draws a label row above it whenever label is not empty. For input fields that row is the field's caption. For elements that show their own text (a button, a card, a picture) it is a second, redundant caption in bold, and it is the most common reason an AI-built view looks unfinished.
Rule: a visible word belongs to exactly one property, and it is the one the element is built to show.
| Element | Visible text goes in | label |
|---|---|---|
text, number, textarea, select, radio, datepicker and every other input | label, in plain words: "Company code" | the field caption |
button | text | "" |
badge | text | "" |
statsCard | title (the metric name) and valueText | "" |
messageCard | title and descriptionText | "" |
pageTitle | title and subtitle | "" |
image | alt (always) and optionally caption | "" |
chart | its title | "" |
divider, spacer, emptyBlock, customHtml, richTextViewer, video, iframe, progressBar, avatar, icon, breadcrumbs | their own content | "" |
singleCheckbox | the statement next to the box in checkboxLabel: "I agree to the terms" | "", or a question when the box answers it |
toggleSwitch | label for what it switches, trueLabel / falseLabel for the states | the caption |
panel, dynamicPanel | label is the panel heading | "" when the panel only groups fields |
page | label is the page title | "" hides the title |
Since 0.10.6, addElement without a label already leaves these display elements without one, turns a button's label into its text, and gives a named field a readable label (companyCode becomes "Company code"). Older builders do not, so set the properties yourself either way.
{ "op": "addElement", "type": "statsCard", "name": "revenue", "properties": { "label": "", "title": "Revenue this month", "valueText": "€48,200" } }Other caption rules:
- Write labels for people, in sentence case and the language of the view: "Delivery date", not
deliveryDate, "DELIVERY DATE" or "Delivery Date". - Never show a technical name. A label equal to the element's
name(el3,customerEmail) means the label was forgotten. placeholderis an example of the answer ("name@company.com"), not a second label.descriptionis for help that does not fit the label. Do not repeat the label in either.
Buttons
- The caption is
text;labelis"". A caption left inlabelis drawn twice: once as a field caption above the button, once on the button. - One primary action per area:
variant: "solid",tone: "primary". Secondary actions next to it arevariant: "outline"withtone: "neutral". A destructive action istone: "risk". - A dark fill needs light text. A solid button with a dark tone (
primary,success,info,risk) or a dark customcolorgetstextColor: "var(--nvb-color-neutral-000)". Before 0.10.6 some host stylesheets left the caption dark on dark; setting it explicitly is correct on every version. A light fill (warning, or a light customcolor) gets dark text,"var(--nvb-color-neutral-900)". - Button bars go in an
emptyBlock, not in a panel row:contentDisplay: "grid",gridTemplateColumns: "max-content max-content", acontentJustifyand acontentGap. Flex does not line them up, because every row inside the block is full width. - A button does something. Give it
events(asubmit, avalidate, a data source call, a toast), or leave it out.
{
"type": "button",
"name": "register",
"label": "",
"text": "Register",
"variant": "solid",
"tone": "primary",
"textColor": "var(--nvb-color-neutral-000)",
"events": [{ "trigger": "click", "type": "submit", "validateForm": true }]
}Color and contrast
- Use the theme's tokens,
var(--nvb-color-primary-600),var(--nvb-color-neutral-100),var(--nvb-color-risk-500)and so on, rather than hex values. The view then follows the host's theme and dark mode. - Text on a filled surface always contrasts with it: light text on a dark fill, dark text on a light fill. Check every place you set a background (
coloron a button,panelBackgroundColoron a block). - Use tones for meaning, not decoration:
successfor done,warningfor attention,riskfor errors and destructive actions,infofor neutral notes.
Layout
- Follow the layout model: children of a container live in the column that references it, never inside the element definition.
- Put related short fields side by side: first and last name, city and postcode, start and end date. Two or three fields per row on desktop; long text fields get a row of their own.
- Elements in one row need no width. Every element without a
widthgrows to fill its share of the row: two elements take half each, three take a third each, and they stretch with the screen. Do not writewidth: "50%"or"33%"to get that. It adds nothing, and it breaks the row:%widths ignore the gap between columns, so two50%elements no longer fit side by side and the second one wraps to the next line. - Only an uneven split gets a width, and only on one element: for a 60/40 pair give the narrow one
width: "40%"and leave the wide one without a width, so it takes the rest.
{
"rows": [
{ "columns": [{ "elementRef": "firstName" }, { "elementRef": "lastName" }] }
]
}firstName and lastName carry no width: each takes half the row.
- Give every side-by-side field
mobileWidth: "100%"so the row stacks on a phone. - Align with
emptyBlock(grid, flex, gap, padding, background). Do not fake alignment withcustomHtml, spacer stacks or extra panels. - Pages are steps, not sections. A single form with sections is one page with panels or headings. Several pages only when the person should move through steps.
- A dashboard or display page is not a form: set
showValidateButton: falseandshowSubmitButton: falseinsettingsso the Validate and Submit toolbar does not appear.
Names
nameis the data key the application receives. Make it meaningful camelCase:companyCode,deliveryDate,invoiceLines. Never leaveel1,el2.- Names are stable. Renaming a field changes the data the host receives and breaks every expression that uses it; use
renameElement, which updates the references, and only when asked. - Inside an
objectPanelordynamicPanel, child names only need to be unique inside that panel; address them by their path (billing.city).
Fields
- Required means it: set
required: truetogether with arequiredMessagein plain words ("Enter your e-mail address"). A conditional requirement isrequireIf, not a validator. - Check formats with validators (
email,pattern,minLength,min,max) and give each a message that tells the person what to fix. - Pick the choice control by the number of options: two to four short options,
radioorselectButton(all visible, one click); more,select; a long or server-backed list,autocomplete; several answers,checkboxormultiSelect; a yes/no,singleCheckboxortoggleSwitch. - Option
values are stable codes (company,person); optionlabels are what people read. - Pre-fill with
defaultValue, notvalue. - Use the specific element for the data:
datepickerfor dates,numberfor amounts,phoneInputfor phone numbers,fileUploadfor files. Atextfield loses validation and formatting the specific one gives for free.
Logic
- Conditions reference fields by
namein braces:{clientType} == "company". - Logic that must react while the person is typing or choosing needs
logicExecutionMode: "onChange"on the element that carries it; the default waits for blur. - A field hidden by
visibleIfusually should not keep its value: setresetOnHide, or aresetIfwith the same condition. - An
expressioncomputes a value from other fields. It never reads its own field. - Show or hide a whole group by putting
visibleIfon its panel, not on every field inside it.
Tables and repeating data
A table starts basic
- Unless the person asked for more, a
tableis columns, paging and the quick search box, nothing else. Do not improvise features. The element also turns on detailed search and export by default, so a basic table switches those two off:showDetailedSearch: false,showExport: false. Quick search (showQuickSearch) stays on. - Add a feature only when it was asked for: detailed search, export, column settings, saved filters, selection, expandable rows, inline editing, a header menu. If a feature seems useful but was not mentioned, suggest it in your summary instead of adding it.
- Row actions always go in the three-dots menu:
rowActionswithrowActionsDisplayMode: "dropdown". UseiconButtonsorbuttonsonly when the person explicitly asks for separate buttons in the row. - The actions column has no header text:
showActionsHeaderLabel: false. It defaults totrueand puts an "Actions" heading over the last column, which the three dots already explain. - When the person does not say how a record opens, it opens from the three-dots menu at the end of the row: one Preview row action. Do not add navigation on a row click (
rowClickActions,rowClickOpensDetails) on your own. When the person does say how (a click on the row, a dialog, a details panel), build it that way.
{
"type": "table",
"name": "orders",
"label": "Orders",
"showDetailedSearch": false,
"showExport": false,
"columnsConfig": [
{ "key": "number", "label": "Order", "type": "text", "showInTable": true },
{ "key": "customer", "label": "Customer", "type": "text", "showInTable": true },
{ "key": "total", "label": "Total", "type": "number", "showInTable": true }
],
"rowActionsDisplayMode": "dropdown",
"showActionsHeaderLabel": false,
"rowActions": [
{ "label": "Preview", "icon": "visibility", "type": "navigate", "navigateTo": "/orders/{row.number}" }
]
}Choosing and filling
dynamicTablefor rows the person edits,tablefor data shown from a data source,dynamicPanelfor a repeating block of several fields per entry.- A status, badge, toggle or button in a
tablecolumn is a hosted element (type: "element"withelementType), not HTML in a template. - Limit rows with
maxRows, or conditionally withdisallowAddRowsIf; protect some rows from deletion withdisallowDeleteRowsIf({row.status} == "approved"), rather than locking the whole table. - A
dynamicPaneldefines its children in itstemplatemap and lays them out incolumn.rows.
Text and content
- One language per view, the view's language, including placeholders, messages and button captions.
- Examples and sample data use neutral, international names and places (Sam Carter, Harbor Foods, Amsterdam).
- Short, specific wording: "Save order", not "Submit"; "Enter your e-mail address", not "Invalid input".
Final checklist
Before you report back, check that:
- No label shows a technical name, and no display element (button, badge, card, image, divider, chart) has a label row.
- Every button has its caption in
text,label: "", an action inevents, and readable text on its fill. - Related short fields share rows without a
width(no50%), and side-by-side fields stack on mobile. - Every
tableis basic unless more was asked for (quick search on, no detailed search, no export), its row actions sit in the three-dots menu with no header over that column, and when the person did not say how a record opens, it opens from a Preview action in that menu, not from a row click. - Every name is meaningful camelCase; no
el1is left. - Required fields have a
requiredMessage; validators have messages. - Live logic has
logicExecutionMode: "onChange", and hidden fields do not keep stale values. - Colors are theme tokens, and text contrasts with every fill.
- Every choice you were unsure about was asked, not guessed, and the choices you made yourself are named in your summary.
- Nothing was saved, and the person was told what changed.