OcaltQL UX Components
UX generates a finished interface component — markup, styling and behaviour together — as a native OcaltQL HTML node. It is to interfaces what GRAPH is to charts: one verb, a type string, your data, and a real renderable result. No CSS framework is loaded, no template is fetched, and nothing is shelled out to.
Syntax
UX TYPE "footer" WITH ?data
STYLE "minimal"
THEME "dark"
ICONS "social"
FORMAT "100%" BY 120
SET ?footer
Every modifier is optional. Minimal form:
UX TYPE "nav" WITH ["Home" AS "/" AND "Docs" AS "/docs" AND "Pricing" AS "/pricing"] SET ?nav
AFTER EMIT ?nav
Modifiers
| Modifier | Syntax | Description |
|---|---|---|
TYPE | TYPE "card" | Which component. Required. |
WITH | WITH ?data | The content. An inline literal, an object, an array, or another HTML node. |
STYLE | STYLE "minimal" | Variant of that component. Every type carries its own set — see the catalog. |
THEME | THEME "dark" | Colour palette. "dark", "light", "ocean", "pastel" — the same four GRAPH uses, so a chart and a card in one page match. |
FORMAT | FORMAT "100%" BY 120 | Width by height. Accepts pixels or a percentage string. |
ICONS | ICONS "social" | Which icon set a component draws from when it renders icons of its own. |
COLOR | COLOR "#0057FF" | Accent colour, overriding the theme's. Every colour format used elsewhere in OcaltQL is accepted. |
ID | ID "main-nav" | Sets the root element's id, so HTML APPEND ... WHERE ID and HTML GET can target it. |
A Component Is Just HTML
This is the part worth reading twice. UX returns the same native node type as NEW HTML, NEW HTML NODE, HTML REVERSE and GRAPH. It is not a special object with its own rules — it is a tree, and every HTML verb already knows what to do with it.
NEW HTML SET ?page
OPEN
HTML "body"
OPEN
HTML "div" ATTR "id" WITH "shell"
CLOSE
CLOSE
AFTER UX TYPE "header" WITH ["title" AS "Ocalt"] THEME "dark" SET ?header
AFTER HTML APPEND ?header INTO ?page WHERE ID "shell"
AFTER EMIT ?page
UX TYPE "card" WITH ["title" AS "Q1 Revenue"] SET ?card
AFTER GRAPH TYPE "line" WITH ?data SET ?svg
AFTER HTML APPEND ?svg INTO ?card WHERE ID "card-body"
AFTER EMIT ?card
(* A native SVG chart, inside a native UX card, both built by verbs *)
FETCH "https://example.com/notice" SET ?raw
AFTER HTML REVERSE ?raw SET ?fragment
AFTER UX TYPE "modal" WITH ["title" AS "Notice"] SET ?modal
AFTER HTML APPEND ?fragment INTO ?modal WHERE ID "modal-body"
AFTER EMIT ?modal
WITH accepts a node. Anywhere a component takes content, it takes an HTML node as readily as a string — so UX TYPE "panel" WITH ?svg and UX TYPE "panel" WITH "text" are both valid. A component is a shell with slots; what goes in the slots can come from anywhere in the language.HTML REVERSE gives back markup, not components. Parsing a page that was built from UX returns an ordinary node tree — correct, complete, and fully appendable, but the runtime does not reconstruct which parts were once components. The composition runs one way. Keep the values that produced a page if you intend to rebuild it.Slots
Every component exposes named regions with stable ids, so HTML APPEND ... WHERE ID can target them without you inspecting the generated markup. Slot ids are the component name plus the region.
| Component family | Slots |
|---|---|
Panels — card, modal, panel, drawer, alert | -header, -body, -footer |
Layout — header, navbar, sidebar, footer | -start, -center, -end |
Containers — grid, stack, split, container | -body, plus -cell-n per child region |
| Every component | -root — the outermost element |
UX TYPE "header" ID "top" SET ?header
AFTER UX ICON "menu" SET ?menuicon
AFTER HTML APPEND ?menuicon INTO ?header WHERE ID "header-start"
AFTER UX TYPE "search" STYLE "compact" SET ?search
AFTER HTML APPEND ?search INTO ?header WHERE ID "header-center"
AFTER UX TYPE "avatar" WITH ["src" AS "/me.png"] SET ?avatar
AFTER HTML APPEND ?avatar INTO ?header WHERE ID "header-end"
Data Tiers
Component data follows the same three-tier pattern as chart data.
Tier 1 — inline key/value pairs, for components whose content is a flat list (nav, menu, breadcrumb, chip, badge, stat):
UX TYPE "breadcrumb" WITH ["Home" AS "/" AND "Docs" AS "/docs" AND "UX" AS "/docs/ux"] SET ?crumbs
Tier 2 — a single object of named properties, for most components:
NEW OBJECT SET ?data
AFTER SET ?data("title") AS "Engineering Magic"
AFTER SET ?data("subtitle") AS "OcaltQL builds the interface too"
AFTER SET ?data("cta") AS "Get started"
AFTER SET ?data("href") AS "/start"
AFTER UX TYPE "hero" WITH ?data STYLE "centered" THEME "dark" SET ?hero
Tier 3 — an object containing arrays of objects, for components with repeating rows or sections (table, list, accordion, tabs, timeline, steps, gallery):
SELECT ROWS FROM DB "shopdb" TABLE "orders" LIMIT 20 SET ?rows
AFTER NEW OBJECT SET ?data
AFTER SET ?data("columns") AS ["id" AND "item" AND "status"]
AFTER SET ?data("rows") AS ?rows
AFTER UX TYPE "table" WITH ?data STYLE "striped" SET ?table
AFTER EMIT ?table
Icons
UX ICON returns one icon as a pure <svg> node — no wrapper, no component around it. It composes like any other node.
UX ICON "search" SET ?icon
AFTER UX ICON "search" SIZE 24 COLOR "#0057FF" STROKE 1.5 SET ?styled
AFTER UX ICON "check" SIZE 16 COLOR "green" STYLE "filled" SET ?tick
AFTER EMIT ?styled
| Modifier | Description |
|---|---|
SIZE n | Square edge in pixels. Default 20. |
COLOR "..." | Stroke or fill colour, any OcaltQL colour format. |
STROKE n | Line weight for outline icons. Default 1.5. |
STYLE "outline"|"filled"|"duotone" | Icon treatment. Default "outline". |
UX ICONS "set" SET ?names returns every icon name in a set as an array, so a script can enumerate what is available rather than guessing. Sets: "core", "social", "media", "file", "arrow", "device", "finance".Animation
UX ?component ANIMATE attaches motion to an existing component. The stage names and types are exactly those on Presentations & Animations — one vocabulary across the platform.
UX TYPE "card" WITH ?data SET ?card
AFTER UX ?card ANIMATE ENTER "fly-in-left" DELAY "0s" DURATION "0.4s"
AFTER UX ?card ANIMATE EMPHASIS "pulse" DELAY "2s" DURATION "0.5s"
AFTER UX TYPE "toast" WITH ["text" AS "Saved"] SET ?toast
AFTER UX ?toast ANIMATE ENTER "fade-in" DURATION "0.3s"
AFTER UX ?toast ANIMATE EXIT "fade-out" DELAY "4s" DURATION "0.3s"
"fade-in", "fly-in-left", "fly-in-right", "fly-in-top", "fly-in-bottom", "zoom-in", "bounce-in", "wipe-in". EXIT — "fade-out", "fly-out-left", "fly-out-right", "fly-out-top", "fly-out-bottom", "zoom-out", "wipe-out". EMPHASIS — "pulse", "spin", "grow", "shrink", "shake".Transitions Between States
A slide transitions once, in one direction. An interface component moves between named states and back again, so UX ... TRANSITION takes both ends.
UX TYPE "sidebar" WITH ?links ID "side" SET ?side
AFTER UX ?side TRANSITION FROM "collapsed" TO "expanded" STYLE "slide" DURATION "0.3s"
UX TYPE "drawer" WITH ?content ID "cart" SET ?cart
AFTER UX ?cart TRANSITION FROM "closed" TO "open" STYLE "slide-right" DURATION "0.25s"
AFTER UX ?cart TRANSITION FROM "open" TO "closed" STYLE "slide-right" DURATION "0.2s"
AFTER UX ?cart STATE "closed"
(* STATE sets which state the component renders in initially *)
STYLE on a transition names the movement: "slide", "slide-left", "slide-right", "slide-up", "slide-down", "fade", "dissolve", "zoom", "flip", "none". The generated CSS carries both directions, so the component moves back without a second definition.Making It Do Something
A component is HTML, so it binds like HTML. HTML GET selects it and ONBROWSER JAVASCRIPT attaches behaviour — there is no separate UX event system to learn.
UX TYPE "button" WITH ["text" AS "Save"] ID "save" SET ?btn
AFTER NEW ONBROWSER AJAX TO "/api" NAMED "save" ON SUCCESS `UX.state('cart','open');` SET ?ajax
AFTER HTML "script" TEXT ?ajax
AFTER HTML GET "#save" SET ?el
AFTER ONBROWSER JAVASCRIPT ?el ONCLICK ENTER `save({ item: 'widget' });`
UX.state(id, name) is the one piece of browser-side API. Any component with declared transitions exposes it: call it with the component's ID and a state name and the component moves there, using whatever TRANSITION was defined between the two. That is the whole runtime surface — everything else is ordinary DOM.Declared Ids and States — the Manifest
UX.state('cart','open') is two strings. Nothing in that call proves cart exists or that open was ever declared, and a rename elsewhere in the script breaks it silently. The runtime already knows every id and every state at the moment it generates the markup, so it publishes them rather than discarding them.
UX TYPE "drawer" WITH ?content ID "cart" SET ?cart
AFTER UX ?cart TRANSITION FROM "closed" TO "open" STYLE "slide-right" DURATION "0.25s"
AFTER UX ?cart STATES SET ?states
AFTER EMIT ?states
(* ["closed" AND "open"] — the states this component actually has *)
UX MANIFEST SET ?manifest
AFTER FOREACH ?manifest SET ?id AS ?entry
OPEN
EMIT ?id & ": " & [JOIN ?entry("states") WITH ", "]
CLOSE
(* cart: closed, open
side: collapsed, expanded *)
Because the manifest is ordinary OcaltQL data, a script can be written against it instead of against literals — which is the difference between a rename breaking loudly at generation time and breaking silently in a browser at 2am.
UX TYPE "drawer" WITH ?content ID "cart" SET ?cart
AFTER UX ?cart TRANSITION FROM "closed" TO "open" STYLE "slide-right" DURATION "0.25s"
AFTER UX ?cart STATES SET ?states
AFTER HTML GET "#opencart" SET ?btn
AFTER ONBROWSER JAVASCRIPT ?btn ONCLICK ENTER `UX.state('cart','` & ?states(1) & `');`
(* If the state is renamed, this line follows it — there is no literal to fall out of date *)
UX.state() with an unknown id or an undeclared state reports what was asked for and what is actually available, rather than doing nothing. There is no silent no-op.ID becomes a real id attribute, so two components with the same ID collide exactly as two hand-written elements would. The manifest surfaces the collision — the same id appearing twice is visible in it — but the namespace is the document's, not something UX owns.Composing a Whole Page
NEW HTML SET ?page
OPEN
HTML "body"
OPEN
HTML "div" ATTR "id" WITH "top"
HTML "div" ATTR "id" WITH "main"
HTML "div" ATTR "id" WITH "bottom"
CLOSE
CLOSE
AFTER UX TYPE "header" WITH ["title" AS "Dashboard"] THEME "dark" SET ?header
AFTER HTML APPEND ?header INTO ?page WHERE ID "top"
AFTER UX TYPE "sidebar" WITH ["Overview" AS "/" AND "Orders" AS "/orders"] ICONS "core" ID "side" SET ?side
AFTER UX ?side TRANSITION FROM "collapsed" TO "expanded" STYLE "slide" DURATION "0.3s"
AFTER HTML APPEND ?side INTO ?page WHERE ID "main"
AFTER GROUP BY "status" FROM DB "shopdb" TABLE "orders" SET ?grouped
AFTER GRAPH TYPE "bar" WITH ?grouped TITLE "Orders by Status" THEME "dark" SET ?chart
AFTER UX TYPE "card" WITH ["title" AS "This Week"] THEME "dark" ID "chartcard" SET ?card
AFTER HTML APPEND ?chart INTO ?card WHERE ID "card-body"
AFTER HTML APPEND ?card INTO ?page WHERE ID "main"
AFTER UX TYPE "footer" WITH ["copy" AS "Ocalt (Pty) Ltd"] STYLE "minimal" ICONS "social" THEME "dark" SET ?footer
AFTER HTML APPEND ?footer INTO ?page WHERE ID "bottom"
AFTER EMIT ?page
Component Catalog
Every type, its data tier, and the styles it accepts.
Layout
| Type | Tier | Styles |
|---|---|---|
header | 2 | bar, centered, split, transparent |
navbar | 1 | horizontal, pill, underline, minimal |
sidebar | 1 | full, icons, collapsible, floating |
footer | 2 | minimal, columns, centered, fat |
hero | 2 | centered, split, image, gradient |
grid | 3 | equal, masonry, auto |
stack | 3 | vertical, horizontal, wrap |
split | 3 | half, thirds, sidebar-left, sidebar-right |
container | 3 | narrow, wide, full |
panel | 2 | flat, raised, outlined |
drawer | 2 | left, right, bottom |
Content
| Type | Tier | Styles |
|---|---|---|
card | 2 | flat, raised, outlined, image-top, horizontal |
list | 3 | plain, divided, bordered, interactive |
table | 3 | plain, striped, bordered, compact |
timeline | 3 | vertical, alternating, compact |
accordion | 3 | plain, bordered, separated |
tabs | 3 | underline, pill, enclosed, vertical |
carousel | 3 | slide, fade, peek |
gallery | 3 | grid, masonry, filmstrip |
article | 2 | prose, narrow, documentation |
stat | 1 | plain, boxed, trend |
quote | 2 | plain, bordered, large |
code | 2 | plain, numbered, terminal |
Input
| Type | Tier | Styles |
|---|---|---|
form | 3 | stacked, inline, grid, floating |
field | 2 | outlined, filled, underline |
textarea | 2 | outlined, filled, autogrow |
select | 1 | outlined, filled, native |
checkbox / radio / toggle | 1 | plain, boxed, switch |
slider | 2 | plain, ticks, range |
search | 2 | bar, compact, expanding |
upload | 2 | button, dropzone, avatar |
datepicker | 2 | calendar, inline, range |
button | 2 | solid, outline, ghost, link, icon |
buttongroup | 1 | joined, spaced, segmented |
Feedback
| Type | Tier | Styles |
|---|---|---|
modal | 2 | centered, sheet, fullscreen |
toast | 2 | top, bottom, corner |
alert | 2 | info, success, warning, error |
banner | 2 | bar, floating, dismissible |
badge | 1 | solid, subtle, dot |
progress | 2 | bar, ring, steps |
spinner | 2 | ring, dots, bar |
skeleton | 2 | text, card, table |
tooltip | 2 | dark, light, arrow |
empty | 2 | plain, illustrated, action |
Navigation
| Type | Tier | Styles |
|---|---|---|
nav | 1 | horizontal, vertical, pill |
menu | 3 | plain, grouped, nested |
breadcrumb | 1 | slash, chevron, arrow |
pagination | 2 | numbered, simple, loadmore |
steps | 3 | horizontal, vertical, numbered |
dropdown | 3 | plain, grouped, rich |
avatar | 2 | circle, square, group, initials |
chip | 1 | solid, outline, removable |
Full Verb Reference
| Verb | Description |
|---|---|
UX TYPE "name" WITH ?data STYLE THEME FORMAT ICONS COLOR ID SET ?node | Generate a component as a native HTML node |
UX ICON "name" SIZE n COLOR "..." STROKE n STYLE "..." SET ?svg | One icon as a pure SVG node |
UX ICONS "set" SET ?names | Every icon name in a set, as an array |
UX ?component ANIMATE ENTER|EXIT|EMPHASIS "type" DELAY "..s" DURATION "..s" | Attach motion — same vocabulary as Presentations & Animations |
UX ?component TRANSITION FROM "state" TO "state" STYLE "..." DURATION "..s" | Define movement between two named states, both directions |
UX ?component STATE "name" | Which state the component renders in initially |
UX ?component STATES SET ?arr | The states this component declares, as an array |
UX MANIFEST SET ?manifest | Every component generated in this execution, keyed by id, with its states |
UX.state(id, name) | Browser-side: move a component to a declared state. Reports valid states on an unknown one. |
How the Styling Travels
A component needs real CSS — pseudo-classes, media queries, keyframes, state rules — none of which an inline style attribute can express. So the styling is a stylesheet, not inline attributes. The question that raises is when it appears, and OcaltQL has no HTML document by default: a script emits whatever it emits, and most scripts emit no page at all.
The rule is therefore per execution, not per document: the stylesheet is written once, the first time a UX component is actually emitted, and never again in that execution. A component that is built but never emitted contributes nothing. Emitting the same component ten times emits its styling once.
UX TYPE "card" WITH ?data SET ?card
AFTER EMIT ?card
AFTER EMIT ?card
(* The stylesheet precedes the first card. The second card is markup only. *)
NEW HTML SET ?page
OPEN
HTML "body"
OPEN
HTML "div" ATTR "id" WITH "slot"
CLOSE
CLOSE
AFTER UX TYPE "card" WITH ?data SET ?card
AFTER HTML APPEND ?card INTO ?page WHERE ID "slot"
AFTER EMIT ?page
(* ?page is a full document, so the stylesheet lands in its head *)
NEW HTML value is wrapped in <!DOCTYPE html> when it is emitted, while a NEW HTML NODE fragment is not — the decision belongs to EMIT, not to the moment the value was built. UX styling follows the same rule: EMIT decides, once.<html>, no <head>, no <body>. That is what makes a component usable as an AJAX response dropped into a page that already exists.HTML APPEND puts it into a document and puts a document into it; HTML GET selects it; ONBROWSER JAVASCRIPT binds it; EMIT serializes it; Site Mode serves it. Nothing about UX is a separate world.