Summary#
There is one archetype: the component. A bounded reactive unit with typed props, reactive members, and a
declarative render tree. A page, a layout, a dialog, a reusable card — all the same declaration, told apart by
attributes on it:
component Card(string title) { … } // a reusable piece
[Page("/orders/{id}")] component Order(Guid id) { … } // …the same thing, with a route
[Composable] component Badge(string text) { … } // …composable into a public pageInside one, the four kinds of member cover everything a screen does — and there is no state keyword:
| You want | You write |
|---|---|
| a value the component owns | a field — int count = 0; |
| a value that follows the database, or another value | live var — live var rows = Order.Where(o => o.Open); |
| something that happens when the user acts | action — action Add() { count = count + 1; } |
| a pure helper the tree may call | method — and render may call it (Calling helpers from render) |
| what is on the screen | render { } — the tree |
And five layers sit under that, which is the other half of the model: almost every question about the UI is really a question about which layer you are in.
| Layer | What it decides | You write |
|---|---|---|
| Theme | the app's design values — colour, spacing, radius, type | theme T { Colors { … } } |
| Style | what one box looks like, in a closed vocabulary of props | variants { base { Bg = Surface; } } |
| Structure | what is on the screen and how it is arranged | render { Stack { Text("Hi"); } } |
| Behaviour | what changes, and when | live var, action, data |
| Route | which component is a page, and who may see it | [Page("/orders")] [Authorize(…)] |
They stack in one direction: a route shows a component, whose structure is built from atoms, each styled by style props, whose values come from theme tokens. Nothing skips a layer, and that is the whole design — change a colour in the theme and every box that named it moves, with no recompile of anything else.
All five layers in one small app — read it top to bottom, and each section below tells you more about the layer you just passed:
// 1. THEME — the values, named once. Every token lowers to a CSS custom property.
theme Studio {
Colors { Bg = "#F6F7F9"; OnBg = "#16181D"; Surface = "#FFFFFF"; Border = "#E3E6EA"; Accent = "#4F46E5"; }
Radius { Card = "10px"; }
FontSize { Body = "15px"; Section = "17px"; }
FontWeight { Medium = "600"; }
}
// 1b. MOTION — looping, with no destination state, so it is an `animation` and not a `Transition`.
animation Pulse {
Duration = "2s";
Easing = EaseInOut;
Repeat = Infinite;
0% { Opacity = 1; }
50% { Opacity = 0.5; }
100% { Opacity = 1; }
}
// 2 + 3. STYLE + STRUCTURE — a `variants` recipe (static CSS) around a render tree built from atoms.
[AllowAnonymous]
component Card(string title) {
variants {
// A pseudo-state NESTS inside a variant value — a top-level block here would be a variant DIMENSION, which
// has to match a parameter.
base { Bg = Colors.Surface; Rounded = Radius.Card; P = 4; BorderW = 1; Border = Colors.Border; Hover { Border = Colors.Accent; } }
}
render {
Stack(gap: 2) {
Text(title, fontSize: FontSize.Section, fontWeight: FontWeight.Medium);
Slot;
}
}
}
// 4 + 5. BEHAVIOUR + ROUTE — state an action moves, on a component the router can reach.
[Page("/")]
[AllowAnonymous]
component Home() {
int count = 0;
action Add() { count = count + 1; }
meta { title = "Overview"; }
render {
Stack(gap: 4, p: 6, maxW: "640px", mx: "auto") {
Card("Counter") {
Row(gap: 3, align: Align.Center) {
Text("Clicked " + count + " times", fontSize: FontSize.Body);
Button("Add", onPress: Add);
Box(w: "10px", h: "10px", rounded: Radius.Card, bg: Colors.Accent, animation: Pulse);
}
}
}
}
}Description#
1. Theme names the values, once#
A theme tokens block declares design tokens: Colors, Space, Radius, FontSize, Shadow, Motion,
ZIndex, Length, Breakpoints. Each token is a single value, and each lowers to one CSS custom property — which is
why re-theming needs no recompile of your components, and why a token can carry a per-mode value
(Modes.Of(light: …, dark: …)) that follows the OS dark-mode preference with no flash.
A token can be a whole colour ramp rather than one value — see color palettes. Fonts the app ships are web fonts — shipping a typeface with your app.
⚠ A token is a scalar. Anything with internal structure is not a token: that is why keyframes live in
animation — looping motion with no destination state rather than in the theme, beside entity and component at the top level.
⚑ Ask the compiler, do not guess: osy docs ui-theming is the token vocabulary in full.
2. Style is one closed vocabulary#
style props is the fixed list of style props — Bg, P, Rounded, Position, Shrink, Cursor, … —
each mapping to CSS. The vocabulary is closed and compile-checked: a misspelled Backgroud = Surface is an error,
not a line that silently styles nothing.
You apply them two ways, and they are the same vocabulary either way:
variants { }on a component — the recipe. Compiles to static CSS classes, so it costs nothing at runtime and can carry pseudo-states (Hover { }) and responsive overrides (Cozy { }).- inline on an atom —
Text("x", fontSize: FontSize.Body, color: Colors.Subtle), for a one-off that does not deserve a component.
Values are a number (a step on a scale — P = 4 is 1rem), a keyword from that prop's closed set
(Display = Display.Flex), a theme token by name, or a literal string for props that pass a raw CSS value through.
⚑ Reach for a token, not a literal, whenever the value is part of the design. osy lint flags a raw value written
three or more times (ui-raw-style-literal-repeated) and names the token to declare — a design decision living in N
places is exactly what the theme exists to prevent.
layout primitives is deliberately separate: gap, align and justify are arrangement, not appearance, and take
their own path. If you are asking "how do these sit next to each other", that is the layout page, not this one.
⚑ Do not derive the vocabulary by reading the renderer. osy docs ui-styling prints every style prop in tables by
group; osy docs ui-layout prints gap/align/justify. A prop you invented because it seemed plausible is a
compile error at best and a silently ignored line at worst.
3. Structure is atoms, components and controls#
Three kinds of thing render, and knowing which you want answers most "how do I build X" questions:
- Atoms — the 16 primitives the renderer itself owns:
Stack,Row,Box,Text,Button,Pressable,Input,TextArea,Link,Image,Icon,Svg,Path,Canvas,Markdown,Upload. There is deliberately noCard, noModal, noGridatom. A grid isBox(display: Display.Grid, cols: …); a card is a component you write. The platform widens the style vocabulary rather than shipping components — that is the standing rule, and it is why the atom list stays this short. - Kit controls — Osyrin.Ui (the UI kit): the ready-made styled ones (
Field,Button,Table, …), in scope for every app with nousingand nouse, forkable by declaring a component of the same name. This is what you build a page out of; a bareInputatom has no label, andField("Email", value: email)is the labelled input with the spacing and the accessibility already in it. Pinning a kit version (using Ui@2) pins a version. - Components — component, what you write. Parameters are props; Slot (child content) takes children;
[Composable] — presentational components in public pages governs reuse from a public page; generic component covers
Dropdown<T>; Calling helpers from render is the call syntax; Visitor handles a heterogeneous tree. - Controls — control — foreign UI controls (charts, grids, maps): a foreign widget (chart, data grid, map) implemented in JavaScript and described
to the compiler by a
controlblock, so its call sites type-check like anything else. Its styling knobs are styles — a control's own look knobs, its imperative verbs commands — the verbs a control accepts, its lazy assets chunks — assets a control loads on demand, what it reports about itself probe — what a control says about itself.
App-shipped icons, SVG assets and textures are referenced by name and checked at compile time.
⚑ osy kit lists every bundled control with its signature and a worked example (osy kit <Control> prints its
whole source, which is also how you fork it). osy kit --atoms lists the 16 primitives, each marked container or
not — i.e. whether gap/align/justify apply to it at all.
4. Behaviour is what changes, and when#
The reactivity & lifecycle model is the core: var is a snapshot, live var subscribes — a distinction worth knowing before
you write anything, because a list that never refreshes is almost always a missing live. on mount / on unmount covers
mount/unmount, Pending the in-flight state, skeleton what stands in before the first result
lands, and A failing query what happens when a read fails.
Writing data is creating & saving data, and it is the one place UI differs from a server function: an edit applies
instantly and stays visible while the user keeps working, and UnitOfWork.Commit() is what sends the
accumulated edits to the server atomically. A form commits once, on Save; a page that saves per action (ticking a
to-do is the save) commits in each verb. Both are correct — what is never correct is a page with no Save and no
UnitOfWork.Commit(), where the write is discarded with no error. A read written inside a body is
reading data inside an action; Dialog.Open / Dialog.Ask / Dialog.Confirm / Dialog.Discard is where the unit of work is a choice you make (Inherit vs Root).
Input handling: on change, onEnter, onEscape, debounce, Validation, Clipboard, pointer, keys, drag. Connection is the app's own surface for "the server dropped".
5. A route makes it a page, and decides who may see it#
routes and pages makes a component a page and gives it a URL: [Page("/catalog/{slug}")] captures the segment as a
parameter, [Layout(AppShell)] wraps it, and [Render(CSR)] / [Render(SSR)] chooses whether the first response
already carries the content. Navigation moves between pages.
page authorization (policies) is the access gate — and the default is the important part: a routed component requires auth
unless it says [AllowAnonymous]. Never the other way round. canPress / canEdit / canSee reflects a policy into the
UI (a button that disables itself because the rule says so, rather than because someone remembered to check).
Session.CurrentUser is who is being shown the page; Visitor is the opaque browser id for someone who
has not signed in — a name, never a credential.
Proving a screen works#
A UI test is an ordinary [Test]: Ui — drive the app's UI from a test — Ui.Visit opens a route, Ui.Click presses what a person
would press, Ui.Fill types, and the same Assert.* verbs ask the questions, with your real security rules on.
within: is how you address one row when several read alike. osy docs testing-ui is the page; osy docs testing
is the model underneath it.
Where things are NOT#
The questions that most often send people to the wrong page:
- "How do I space these out?" →
gap/align/justifyare layout primitives, not style props. - "How do I make this move?" → an A→B state change is
Transition(a theme tokens motion token, applied as a style prop). Looping motion with no end state is animation — looping motion with no destination state. - "Why is my list stale?" → The reactivity & lifecycle model. It is
varwhere you wantedlive var, far more often than it is anything else. - "Where do I put the save?" → creating & saving data. There is no
Save()on a row; there isUnitOfWork.Commit()on the unit of work. - "Why is my third-party script blocked?" → What an app page is allowed to load. A control must ship what it needs rather than fetch it at run time.
Read these four first#
If you are starting cold, four pages get you productive and the rest are reference:
- component — how to declare one and render it.
- layout primitives — how boxes sit next to each other.
- theme tokens then style props — in that order: tokens first, then the props that name them.
- The reactivity & lifecycle model — the
var/live vardistinction.
Then routes and pages + page authorization (policies) when you want a real page, creating & saving data the moment it has to save, and control — foreign UI controls (charts, grids, maps) when you need something the atoms cannot express. Writing a component — what differs from C# is worth a skim if you are coming from C#.
The pages#
The whole area, grouped by the question that sends you to it.
The unit itself
- component — the one archetype; props, members, render
- Writing a component — what differs from C# — what a component body does not do the way C# does
- The reactivity & lifecycle model —
varvslive var, and how a change re-renders only what read it - on mount / on unmount —
on mount/on unmount - generic component — a component with type parameters
- [Composable] — presentational components in public pages —
[Composable], for a presentational piece a public page composes
Building the tree
- layout primitives —
Stack/Row/Box, andgap/align/justify - Slot (child content) · Slot(item) — let the caller decide what each row looks like · Cell template (your own content in a control's cell) — taking children, per-item templates, a control's cells
- Naming a value in render — naming a value inside
render - Calling helpers from render — calling a pure helper from a render expression
- Visitor — rendering a heterogeneous tree
- Markdown — rendering markdown text — rendering stored markdown as content
- Canvas — a drawing surface and the
Draw.*verbs - Canvas 3D — a lit, shadowed 3D scene on the same canvas: cameras, lights, fog and meshes
- The app shell (rail, work area, tabs) — the rail/work-area/tabs shell, shipped as a sample you copy
Look
- style props — the style-prop vocabulary
- theme tokens — tokens; color palettes — colour ramps
- animation — looping motion with no destination state — looping motion
- web fonts — shipping a typeface with your app · icons · SVG assets · textures — the assets an app ships
- accessibility —
role:,label:, and the state props
Ready-made
- Osyrin.Ui (the UI kit) — the bundled controls; Pinning a kit version (using Ui@2) — pinning one
- control — foreign UI controls (charts, grids, maps) — declaring a foreign widget
- styles — a control's own look knobs · commands — the verbs a control accepts · chunks — assets a control loads on demand · probe — what a control says about itself — a control's four blocks
- The markdown editor kit — a rich editor you opt into — the optional rich markdown editor
Data
- creating & saving data — creating, editing, deleting, and
UnitOfWork.Commit() - reading data inside an action — a read written inside an action or hook
- Pending · skeleton · A failing query — in-flight, stand-in, and failed
- Sorting by a column the user picks — sorting by a column the user picks
- Validation — the entity's rules, shown beside the field
Input and events
- on change · onEnter · onEscape · debounce — the everyday handlers
- pointer · keys · drag — pointer, held keys, drag-to-a-number
- on every —
on every; on settled — run something once, when a stream finishes — once, when a stream finishes - Clipboard — writing to the system clipboard
- Func<T, R> —
Func<T, R>, so a component can be told how to get a value
Route, access, session
- routes and pages —
[Page], params,[Layout],[Render(CSR)] - Navigation — moving between pages
- page authorization (policies) — the secure-by-default gate; canPress / canEdit / canSee —
canPress/canEdit/canSee - Session.CurrentUser —
Session.CurrentUser; Visitor — the pre-sign-in browser id - Dialog.Open / Dialog.Ask / Dialog.Confirm / Dialog.Discard — an overlay, and which unit of work it edits in
- Connection — the app's connection-loss surface
- What an app page is allowed to load — what a page is allowed to load
Measuring the real box
- Layout.ScrollHeight and Layout.ScrollWidth —
Layout.ScrollHeightvsLayout.Height - Layout.TextWidth — how wide a string will actually paint
See also#
- component — the unit everything else hangs off.
- style props — the full style-prop vocabulary, group by group.
- theme tokens — the token system the style props draw from.
- layout primitives — arrangement, which is deliberately not styling.
- creating & saving data — how a screen writes, and where the commit is.
- Ui — drive the app's UI from a test — driving the screen from a
[Test], with the security rules on.