Design surface (v0.1)

The original v0.1 spec, kept as design history. Not a description of the built runtime.

⚠️ This is the v0.1 design surface, not the built surface. Several things here were never implemented (non-text <input> types, <image> rendering, real scrolling) and some were implemented differently (the inline/block model was removed; grid was added; rhai functions can't mutate state). See As Built for the current reality.

The formal reference for Rux v0.1. This is the source of truth we architect and build against. It describes the language surface, not the runtime implementation (that comes later). Every rule here traces to a law in the rationale.

Status: v0.1 design. Syntax is settled; anything marked deferred is intentionally out of scope for the first build.

Contents


File format: the SFC

A Rux program is one or more single-file components with the extension .rux. Each file has up to three top-level sections, in any order, each at most once:

<template>  <!-- semantic markup, the view tree --> </template>
<style>     /* literal CSS, how it looks and lays out */ </style>
<script>    // rhai (Rust-shaped): logic, signals, handlers </script>
  • <template> is required and must contain exactly one root element.
  • <style> and <script> are optional.
  • A file with a <template> is a component; its file name (kebab-cased) is the tag other files use to embed it. See modules & reuse.

The application entry point is a component whose root is <screen>.


Elements

There are exactly six elements (Law 3). Everything else is a role, an <input type=>, a directive, or CSS.

ElementIsWeb analogueNotable attributes
<screen>root of one view<body>n/a
<view>generic container / box<div>role
<text>text run<span> / <p>role, for
<image>bitmap or vector image<img>src, alt
<button>tappable control<button>disabled
<input>user input control<input>type, r-model, :options, min, max, placeholder, id

Any element accepts: class, id, role (where semantic), style/structural directives, and the events in its capability set.

Input types

<input> absorbs every form control via type= (Laws 3 & 4). There is no <select>, <option>, or <textarea>.

typeControlBinds to
text (default)single-line textstring
textareamulti-line textstring
numbernumeric textnumber
checkboxon/off boxbool
switchon/off togglebool
radioone-of within a name groupstring
sliderrangenumber (min/max/step)
selectchoice from a setstring; choices via :options
datedate pickerdate string

Choices are data, not tags. A select (or a radio group) takes its options from a bound collection:

<input type="select" r-model="choice" :options="fruits" />

On platforms with a native picker (mobile), select and date should invoke the platform control rather than a rendered dropdown, a host capability, not a browser emulation.

Deferred: options that need custom per-item markup (icon + multiline), a "templated input" feature for a later version.


Roles

role= gives an element semantic meaning for accessibility and intent without adding elements. It never affects layout or behavior (Law 1/3). Screen readers and assistive tech consume it; the visual result is still 100% CSS.

Recognized roles (extensible):

CategoryRoles
Structuresection, header, footer, nav, main, aside
Textheading, paragraph, label, link
Collectionslist, listitem
Formsform

Role-specific attributes:

  • role="link"to="/path": a navigation intent handled by the router (ecosystem), not a URL fetch.
  • role="label"for="<input id>": associates the label with an input; tapping the label focuses the input.
  • role="form" → pairs with @submit.
<view role="nav" class="bar">
  <text role="link" to="/settings">Settings</text>
</view>

Directives & bindings

Structural and binding features are attributes. Interpolation uses {{ }}; everything structural is prefixed r- (one prefix, no collisions, which is what lets loop-r-for and label-for= coexist).

SyntaxMeaningVue analogue
{{ expr }}text interpolation (in element bodies){{ }}
:attr="expr"one-way bind an attribute to an expression:attr
r-model="target"two-way bind (form controls)v-model
@event="handler"bind a handler to a capability@click
r-for="item in items"repeat the element per itemv-for
r-if / r-elif / r-elseconditional inclusionv-if
r-show="expr"toggle visibility, keep layout slotv-show

Notes:

  • {{ expr }} and :attr / r-model / r-for accept script expressions evaluated in the component's scope.
  • r-for supports an index form: r-for="(item, i) in items".
  • Elements in an r-for should carry a stable :key where identity matters (reordering, animation).
  • r-if removes the element from the tree; r-show keeps it and toggles visibility, the same distinction Vue draws.
<view role="list" class="feed">
  <view role="listitem" class="tile"
        r-for="d in devices" :key="d.id" @tap="select(d)">
    <text role="heading">{{ d.name }}</text>
    <text role="paragraph" r-if="d.online">online</text>
    <text role="paragraph" r-else>offline</text>
  </view>
</view>

Events

Each element publishes a fixed capability set: the events it can emit (Law 2). Bind with @event. The vocabulary is gesture-first (device-honest); hover is pointer-only and never fires on touch.

ElementCapabilities
<screen>appear, disappear, key, back
<view>tap, longpress, drag, swipe, scroll¹, hover²
<text>tap, select
<image>tap, load, error
<button>tap, press, release, longpress, focus
<input>input, change, focus, blur, submit

¹ scroll only fires if CSS gave the element overflow: auto/scroll, capability follows style. ² hover is pointer-only; binding it on a touch device is legal but silent.

A handler receives an event object with gesture data (position, delta, key, etc.); the exact shape is defined during runtime design.

<button @tap="select(d)" @longpress="pin(d)"></button>
<view @swipe="onSwipe($event)"></view>

Styling

The <style> section is literal CSS, parsed by lightningcss. Selectors, the cascade, and specificity work as on the web. Class and id selectors match class=/id=; element selectors match the six element names; [role=…] attribute selectors match roles.

Honored subset (v0.1)

The runtime parses full CSS but only honors the properties below, mapped onto taffy (layout) and the painter. Unhonored properties are ignored (and should be reported in dev mode), not errors.

GroupProperties
Box modelwidth, height, min/max-width, min/max-height, padding, margin
Flex layoutdisplay: flex, flex-direction, flex-wrap, flex, gap, justify-content, align-items, align-self
Positioningposition: relative/absolute, top/right/bottom/left, overflow, z-index
Background/borderbackground, background-color, border, border-radius, border-color, border-width
Textcolor, font-size, font-weight, font-family, line-height, text-align, letter-spacing
Effectsopacity, box-shadow
Listslist-style (marker rendering for role="list")

Units: px, %, rem, fr (grid), and unitless where CSS allows. Colors: hex, rgb[a](), named. display: grid and its properties are partially honored in v0.1 (single-axis tracks); full grid and display: table are deferred (the table story).

Styles may be inline in <style> or imported (see modules).


Scripting and the host

Logic lives in two tiers (Law 4, and the decision):

Script tier: <script>, interpreted rhai

Rust-shaped syntax (let, fn, closures, if/for). Hot-reloads. Holds the component's signals, handlers, and glue. Everything declared at the top level of <script> is in scope for the template's expressions.

<script>
use components::device_tile;          // import another component (see modules)

let level   = signal(82);             // reactive state
let devices = signal(host::load_devices());

fn refresh() {                        // a handler bound via @tap
    level.set(host::read_battery());
}

fn select(d) {
    host::open(d.id);
}
</script>

Host tier: compiled Rust, exposed as host::…

The registry contract between file and binary. The compiled app registers named capabilities; the script calls them by name. This is the boundary where native, heavy, or performance-critical work lives, and it's the one part that requires a rebuild.

Conceptually, the host registers functions into the script's host namespace:

// compiled Rust (illustrative; final API defined in runtime design)
registry.function("read_battery", || sysinfo::battery_percent());
registry.function("load_devices", || db::all_devices());
registry.function("open", |id: Id| navigator::open(id));

Rules of the contract:

  • The script may only call host:: names that were registered; unknown names are a runtime error surfaced in the dev overlay.
  • Host functions are the only way script reaches native capability (files, network, sensors, navigation, platform pickers).
  • Types cross the boundary as rhai's dynamic values; the host is responsible for validation.

Reactivity

Reactivity is a core primitive (not ecosystem; see the decision). The model is fine-grained signals (Leptos/Solid style): a binding is a subscription; there is no virtual-DOM diffing.

signal

let count = signal(0);

count.get()               // read (also tracks a dependency when read in a binding)
count.set(5)              // replace
count.update(|c| c + 1)   // functional update

How bindings subscribe

When the template compiler encounters {{ count }} (or :attr="count"), it records that this node depends on count. When count changes, only that node re-lays-out and repaints:

{{ device.battery }}  ──parse──►  node subscribes to device.battery

     device.battery.set(80)  ───────► only that node updates

Derived values are computed from other signals and cache until a dependency changes (a computed/memo primitive; exact spelling finalized in runtime design). Stores, routing, and persistence are ecosystem crates built on signal, and not part of this spec.


Modules & reuse

A .rux file is a component. Reuse mirrors Rust's module system (Law 4): import with use, embed as a custom element named after the file.

<script>
use components::device_tile;   // ./components/device_tile.rux
</script>
<template>
  <view role="list" class="feed">
    <device-tile r-for="d in devices" :device="d" @tap="select(d)" />
  </view>
</template>
  • The file device_tile.rux is embedded as <device-tile> (kebab-case of the file/component name).
  • Props pass in as attributes (:device="d"), one-way bound from parent scope to child.
  • A child communicates upward by emitting events its parent binds with @.
  • use paths resolve relative to the importing file, following the same directory rules as Rust modules.

Deferred: slots/children projection (passing template fragments into a component), a later version.


Hot-reload boundary

What reloads live vs. what needs a rebuild. The practical contract for authors:

You edit…Result
<template> markup✅ live repaint
<style> CSS✅ live repaint
<script> logic (rhai)✅ live, state re-initialized
Which host fn a handler calls✅ live
The host (compiled Rust), new capability or changed native fn❌ rebuild

Parse/eval errors in any live section surface as a dev overlay in the window, not a crash (the accepted cost of runtime documents; see the rationale).


Next: the guide builds a real screen using everything here.