TypeScript server-rendered UI framework with real-time WebSocket updates.
t-sui renders UI on the server as JavaScript strings. The browser receives raw JS that performs document.createElement() calls directly — no HTML templates, no JSON intermediate, no client-side framework. SVG elements use document.createElementNS() with proper namespace handling. User interactions trigger server actions via WebSocket, which respond with JS strings for DOM mutations.
- Full API docs:
docs/DOCUMENTATION.md - Assistant skill docs:
docs/skills/SKILL.md
From a specific release (recommended):
npm install https://github.com/michalCapo/t-sui/releases/download/v0.1.0/t-sui-0.1.0.tgzOr latest from git:
npm install github:michalCapo/t-suiThen import:
import ui from "t-sui/ui";
import { MakeApp, type Context } from "t-sui/ui.server";
import { NewForm } from "t-sui/ui.form";
import components from "t-sui/ui.components";Requires Node.js 18+ with tsx for TypeScript execution.
import ui from "t-sui/ui";
import { MakeApp, type Context } from "t-sui/ui.server";
const app = MakeApp("en");
app.Page("/", "Home", function (_ctx: Context) {
return ui.Div("max-w-2xl mx-auto p-6").Render(
ui.Div("text-2xl font-bold").Text("Hello World"),
);
});
app.Listen(1423);Server (TypeScript) Browser
───────────── ───────
PageHandler → Node → .ToJS() → Minimal HTML + <script>
ActionHandler → JS string ←→ WebSocket (__ws)
- Server-centric — all DOM trees built in TypeScript as
Nodeobjects, compiled to JavaScript via.ToJS() - WebSocket-only interactivity — click/submit events call server actions, responses are JS strings executed via
new Function() - Five DOM swap strategies —
ToJS,ToJSReplace,ToJSAppend,ToJSPrepend,ToJSInner - No client framework — the client is a ~120-line WS connector with offline overlay and auto-reconnect
- Tailwind CSS — loaded via browser CDN (
@tailwindcss/browser@4) - Dark mode — built-in theme system (System/Light/Dark) with
ThemeSwitchercomponent - Localization — per-component locale structs; English by default
| File | Purpose |
|---|---|
ui.ts |
Node class, element constructors, JS helpers, ResponseBuilder |
ui.server.ts |
HTTP/WebSocket server, App, Context, routing, SPA navigation |
ui.components.ts |
High-level components (Accordion, Alert, Badge, Card, Tabs, etc.) |
ui.form.ts |
FormBuilder with field types, validation, and Collect-based submission |
ui.table.ts |
SimpleTable and DataTable with search, sort, pagination, filters |
ui.collate.ts |
Collate data panel with filter/sort/search slide-out panel |
ui.data.ts |
Data querying helpers, NormalizeForSearch, filter constants |
ui.protocol.ts |
WebSocket protocol type definitions |
ui.proxy.ts |
HTTP/WS proxy server utilities |
All UI is built with Node objects using PascalCase constructors:
ui.Div("flex gap-2").ID("my-id").Render(
ui.Span("font-bold").Text("Label"),
ui.Span("text-gray-500").Text("Value"),
);- Block:
Div,Span,Button,H1–H6,P,A,Nav,Main,Header,Footer,Section,Article,Aside,Form,Pre,Code,Ul,Ol,Li,Label,Textarea,Select,Option,SVG,Table,Thead,Tbody,Tfoot,Tr,Th,Td,Details,Summary,Dialog,Strong,Em,Small,B,I,U,Blockquote,Figure,Figcaption,Dl,Dt,Dd,Video,Audio,Canvas,Iframe,Picture - Void:
Img,Input,Br,Hr,Wbr,Link,Meta,Source,Embed,Col - Input types:
IText,IPassword,IEmail,IPhone,INumber,ISearch,IUrl,IDate,IMonth,ITime,IDatetime,IFile,ICheckbox,IRadio,IRange,IColor,IHidden,ISubmit,IReset,IArea
.ID(id)— set element ID.Class(cls)— append CSS classes.Text(text)— set textContent (XSS-safe).Attr(key, value)— set HTML attribute.Style(key, value)— set inline style.Render(...children)— append child nodes.OnClick(action)— attach click event.OnSubmit(action)— attach submit event.On(event, action)— attach any event.JS(code)— attach post-render JavaScript
.ToJS()— append todocument.body.ToJSReplace(id)— replace element by ID.ToJSAppend(parentId)— append to parent by ID.ToJSPrepend(parentId)— prepend to parent by ID.ToJSInner(targetId)— replace innerHTML of target by ID
Actions are named handlers registered on the app and called via WebSocket:
// Register action
app.Action("counter.inc", function (ctx: Context) {
const data = { id: "", count: 0 };
ctx.Body(data);
return counterWidget(data.id, data.count + 1).ToJSReplace(data.id);
});
// Attach to element
ui.Button("px-4 py-2 rounded").Text("+1").OnClick({ Name: "counter.inc", Data: { id, count } });Actions are objects: { Name: string, Data?: object, Collect?: string[] }
Name— action name registered withapp.Action()Data— payload sent to the serverCollect— array of element IDs whose values are collected and merged into data
For inline JavaScript: ui.JS("code") returns { rawJS: "code" }
return ui.NewResponse()
.Replace("row-" + id, updatedRow)
.Toast("success", "Updated")
.Navigate("/items")
.Build();ResponseBuilder methods: Replace, Append, Prepend, Inner, Remove, Toast, Navigate, Redirect, Back, SetTitle, JS, Build.
// Push to current client
ctx.Push(ui.SetText("clock", new Date().toLocaleTimeString()));
// Broadcast to all clients
app.Broadcast(ui.Notify("info", "Server restarted"));Standalone JS string generators for common DOM operations:
Notify(variant, message)— toast notification (success/error/info/error-reload)Redirect(url)— navigate viawindow.location.hrefSetLocation(url)— push URL to history without reloadSetTitle(title)— update document titleRemoveEl(id)— remove element by IDSetText(id, text)— set textContent by IDSetAttr(id, attr, value)— set attribute by IDAddClass(id, cls)/RemoveClass(id, cls)— toggle classesShow(id)/Hide(id)— togglehiddenclassDownload(filename, mimeType, base64Data)— trigger file downloadDragToScroll(id)— enable drag-to-scroll on elementBack()— returns an Action forhistory.back()
ui.If(condition, node) // returns node or undefined
ui.Or(condition, yes, no) // returns yes or no
ui.Map(items, fn) // maps items to Node[]Components are in ui.components.ts with builder-pattern APIs:
- Accordion — bordered/ghost/separated variants, single/multiple open
- Alert — info/success/warning/error variants, dismissible
- Badge — color/size presets, dot indicator, icon
- Button presets —
Blue,Red,Green,Yellow,Purple,Gray,White+ outline variants - Card — header/body/footer, 4 variants (shadowed/bordered/flat/glass), hover
- Tabs — underline/pills/boxed/vertical styles, keyboard nav, ARIA
- Dropdown — items, headers, dividers, danger items, 4 positions, auto-close
- Tooltip — 4 positions, 6 color variants, configurable delay
- Progress — gradient, striped, animated, indeterminate, labels
- Step Progress — step X of Y with progress bar
- Confirm Dialog — overlay with confirm/cancel actions
- Skeleton Loaders — table, cards, list, component, page, form
- Theme Switcher — System/Light/Dark toggle
- Icon — Material Icons Round
Declarative FormBuilder with field types and Collect-based submission:
import { NewForm } from "./ui.form";
const form = NewForm("my-form")
.Title("Contact")
.Text("name").Label("Name").Required()
.Email("email").Label("Email").Required()
.OnSubmit({ Name: "contact.save" })
.Build();- 20 field types: text, password, email, number, tel, url, search, date, month, time, datetime-local, textarea, select, checkbox, radio, file, range, color, hidden
- Client-side validation (required, regex pattern, minLength, maxLength)
- Server-side validation with
ValidateForm() - Radio variants: inline, button-style, card-style
- Form-scoped radio names
- Multiple submit/cancel buttons
SimpleTable— quick table with headers, rows, striped/hoverable/bordered/compactDataTable— generic table with search, sort, pagination, column filters, Excel/PDF export
Card/list-style data panel with slide-out filter/sort panel:
- Configurable sort fields and filter types: boolean, date range, select, multi-check
- Debounced search, load-more pagination, export
- Custom row rendering, expandable details
NormalizeForSearch()— accent-insensitive search normalization- Filter constants:
BOOL,NOT_ZERO_DATE,ZERO_DATE,DATES,SELECT TField,TQuerytypes for query modeling
import { MakeApp, type Context } from "./ui.server";
const app = MakeApp("en");app.Page(path, title, handler)— register a page (handler returnsNode)app.Action(name, handler)— register an action (handler returnsstring)app.Layout(handler)— set layout wrapper (use__content__ID for content slot)app.Listen(port)— start HTTP + WebSocket serverapp.GET(path, handler)/app.POST()/app.DELETE()— custom HTTP routesapp.CSS(urls, inline)— add global stylesheetsapp.Assets(dir, prefix)— serve static filesapp.Broadcast(js)— push JS to all connected clientsapp.Title/app.Description/app.Favicon— SEO metadataapp.HTMLHead— custom head elementsapp.Handler()— get the raw HTTP request listener
Use :param syntax:
app.Page("/users/:id", "User", function (ctx: Context) {
const id = ctx.PathParams["id"];
return ui.Div().Text("User " + id);
});ctx.Body(obj)— parse action data into typed objectctx.QueryParam(name)— single query parameterctx.QueryParams(name)— all values for query parameterctx.AllQueryParams()— full query mapctx.PathParams— route parameter mapctx.Session— session data (in-memory, cookie:tsui_sid)ctx.Success(msg)/ctx.Error(msg)/ctx.Info(msg)— queue toast notificationsctx.JS(code)— queue arbitrary JSctx.Build(result)— prepend queued extras to result stringctx.Push(js)— send JS to current WebSocket clientctx.HeadCSS(urls?, inline?)/ctx.HeadJS(urls?, inline?)— per-page head injection
The client exposes __nav(url) for SPA-like navigation. Layout must have a node with ID("__content__"):
app.Layout(function (_ctx: Context) {
return ui.Div("min-h-screen").Render(
ui.Nav("p-4").Render(
ui.Button("px-3 py-1").Text("Home").OnClick(ui.JS("__nav('/')")),
),
ui.Main("p-4").ID("__content__"),
);
});import components from "./ui.components";
components.ThemeSwitcher(); // System -> Light -> Dark toggleUses Tailwind dark: variants. Theme is persisted in localStorage and applied before render to prevent FOUC.
Components use English text by default. Pass locale structs for non-English:
// DataTable
table.Locale({ Search: "Hledat...", Apply: "Pouzit", NoData: "Zadna data" });
// Collate
collate.Locale({ Filter: "Filtr", Reset: "Obnovit", SortBy: "Radit dle" });HTTP/WebSocket reverse proxy for development:
import { startProxyServer, stopProxyServer, getProxyStatus } from "./ui.proxy";
await startProxyServer({ ProxyPort: "8080", TargetHost: "localhost", TargetPort: "1423" });- JS string escaping — all embedded strings escaped via
escJS() - textContent —
Text()usestextContent, notinnerHTML, preventing XSS - Panic recovery — server panics surface as error toasts
- WebSocket-only — no form submissions or XHR
- Auto-reconnect — offline overlay with automatic retry
npm run dev
# Open http://localhost:1423The example app includes 23+ pages demonstrating components, forms, tables, data panels, real-time updates, navigation, and more.
- App setup and route registration:
examples/app.ts - Local dev entrypoint:
examples/main.ts - Example pages:
examples/pages - Example tests:
examples/tests
npm testThe deploy script bumps the version, runs checks/tests, creates a git tag, and publishes a GitHub release with the installable tarball:
./deployMIT