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.

Native rendering. Component geometry, theming, and the CSS and JavaScript each component needs are computed inside the OcaltQL runtime, matching the hand-built precedent of the JSON parser, the HTML parser and the chart renderer elsewhere in this platform.

Syntax

Full Form
UX TYPE "footer" WITH ?data
STYLE "minimal"
THEME "dark"
ICONS "social"
FORMAT "100%" BY 120
SET ?footer

Every modifier is optional. Minimal form:

Minimal Form — Inline Data
UX TYPE "nav" WITH ["Home" AS "/" AND "Docs" AS "/docs" AND "Pricing" AS "/pricing"] SET ?nav
AFTER EMIT ?nav

Modifiers

Modifier Syntax Description
TYPETYPE "card"Which component. Required.
WITHWITH ?dataThe content. An inline literal, an object, an array, or another HTML node.
STYLESTYLE "minimal"Variant of that component. Every type carries its own set — see the catalog.
THEMETHEME "dark"Colour palette. "dark", "light", "ocean", "pastel" — the same four GRAPH uses, so a chart and a card in one page match.
FORMATFORMAT "100%" BY 120Width by height. Accepts pixels or a percentage string.
ICONSICONS "social"Which icon set a component draws from when it renders icons of its own.
COLORCOLOR "#0057FF"Accent colour, overriding the theme's. Every colour format used elsewhere in OcaltQL is accepted.
IDID "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.

A Component Goes Into a Document
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
And a Document Goes Into a Component
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 *)
Fetched Markup, Inside a Component
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
Filling Three Slots of One Header
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):

Tier 1
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:

Tier 2
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):

Tier 3 — Straight From the Database
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.

An Icon, Plain and Styled
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 nSquare edge in pixels. Default 20.
COLOR "..."Stroke or fill colour, any OcaltQL colour format.
STROKE nLine 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.

Entrance, Emphasis, Exit
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"
ENTER"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.

Collapsed to Expanded
UX TYPE "sidebar" WITH ?links ID "side" SET ?side
AFTER UX ?side TRANSITION FROM "collapsed" TO "expanded" STYLE "slide" DURATION "0.3s"
Several States on One Component
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.

A Button That Calls Your Own API
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.

Read Back What a Component Declares
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 *)
Every Component on the Page
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.

Generate the Call From the Manifest, Not From Memory
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 *)
A bad call fails loudly. The manifest is emitted alongside the components, so 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.
Ids share the document's namespace. A component's 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

Shell, Sidebar, Content, Footer
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

TypeTierStyles
header2bar, centered, split, transparent
navbar1horizontal, pill, underline, minimal
sidebar1full, icons, collapsible, floating
footer2minimal, columns, centered, fat
hero2centered, split, image, gradient
grid3equal, masonry, auto
stack3vertical, horizontal, wrap
split3half, thirds, sidebar-left, sidebar-right
container3narrow, wide, full
panel2flat, raised, outlined
drawer2left, right, bottom

Content

TypeTierStyles
card2flat, raised, outlined, image-top, horizontal
list3plain, divided, bordered, interactive
table3plain, striped, bordered, compact
timeline3vertical, alternating, compact
accordion3plain, bordered, separated
tabs3underline, pill, enclosed, vertical
carousel3slide, fade, peek
gallery3grid, masonry, filmstrip
article2prose, narrow, documentation
stat1plain, boxed, trend
quote2plain, bordered, large
code2plain, numbered, terminal

Input

TypeTierStyles
form3stacked, inline, grid, floating
field2outlined, filled, underline
textarea2outlined, filled, autogrow
select1outlined, filled, native
checkbox / radio / toggle1plain, boxed, switch
slider2plain, ticks, range
search2bar, compact, expanding
upload2button, dropzone, avatar
datepicker2calendar, inline, range
button2solid, outline, ghost, link, icon
buttongroup1joined, spaced, segmented

Feedback

TypeTierStyles
modal2centered, sheet, fullscreen
toast2top, bottom, corner
alert2info, success, warning, error
banner2bar, floating, dismissible
badge1solid, subtle, dot
progress2bar, ring, steps
spinner2ring, dots, bar
skeleton2text, card, table
tooltip2dark, light, arrow
empty2plain, illustrated, action

Navigation

TypeTierStyles
nav1horizontal, vertical, pill
menu3plain, grouped, nested
breadcrumb1slash, chevron, arrow
pagination2numbered, simple, loadmore
steps3horizontal, vertical, numbered
dropdown3plain, grouped, rich
avatar2circle, square, group, initials
chip1solid, outline, removable

Full Verb Reference

Verb Description
UX TYPE "name" WITH ?data STYLE THEME FORMAT ICONS COLOR ID SET ?nodeGenerate a component as a native HTML node
UX ICON "name" SIZE n COLOR "..." STROKE n STYLE "..." SET ?svgOne icon as a pure SVG node
UX ICONS "set" SET ?namesEvery 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 ?arrThe states this component declares, as an array
UX MANIFEST SET ?manifestEvery 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.

Emitted Bare — No Document Involved
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. *)
Inside a Document — It Goes in the Head
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 *)
This mirrors what NEW HTML already does. A 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.
A fragment is still a fragment. Emitting a bare component gives you the stylesheet and the markup, nothing more — no <html>, no <head>, no <body>. That is what makes a component usable as an AJAX response dropped into a page that already exists.
A component is an HTML node, and that is the whole design. 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.