Skip to content

Server-driven UI

Syntara as a JSON contract: a schema per component, a validator, and one web renderer.

Some apps don’t build their screens into the app. The server sends each screen as JSON and the app draws it, so a new layout reaches people without an app release. For those apps, a design system also has to exist as a schema: which components a server may send, with which props, and what a client does with anything it doesn’t know.

@syntara/sdui is that schema for Syntara. It’s generated from the same meta.json files as these docs, so the contract and the components can’t drift apart. It comes with a validator for the server and one reference renderer for the web. There is no native renderer (ADR-019).

Try it

The editor holds the document a server would send, and the screen is what <SyntaraScreen> draws from it. Change the tenant in the toolbar: the screen takes on the new brand, and the JSON stays the same. Then use Break it to see what the validator and the renderer do with a bad document.

Break it
Rendered screenVela · en-IN · light · compact

Savings account ending 4821

Good morning, Priya

Available balance
₹1,84,250
+6.4% bettervs last month
Spent this month
₹32,480
+12% worsevs last month
Reward points
2,140
Worth ₹535 on your next bill
Update your KYC by 15 October
Your PAN details need a fresh check. UPI payments pause after the due date until it's done.

Recent activity

Last 3 transactions

Groceries

Today, 09:12 · UPI

−₹1,240

Salary credited

1 Oct · NEFT

+₹96,500

Electricity bill

29 Sep · Autopay

−₹2,318

Validator

Strict, on the server · validateScreen

Valid

Renderer

Tolerant, on the client · onIssue

Drew the screen

Host

The app decides what to do · onAction

Nothing yet. Press a button or link in the screen. Links don’t navigate: the host gets them.

The validator is strict because it runs on the server, before a screen ships. The renderer is tolerant, because a server may already speak a newer version than the app on someone’s phone. So an unknown component with a fallback fails validation, but a client still draws its fallback. A missing accessible name or an unsafe link fails both, and the client shows its own fallback screen instead.

Direction follows the language of the copy. Each example says "locale": "en-IN", so its screen runs left to right even when the client around it is right to left. English copy in a right-to-left layout would be reordered by the browser: a minus sign or a final full stop moves to the other end. Delete the locale line from the JSON and the screen follows the client instead: switch to right to left and the layout mirrors. A server that knows its reader sends Arabic copy with an Arabic locale.

'use client';
import { ThemeScope } from '@syntara/react';
import { SyntaraScreen } from '@syntara/sdui/react';

<ThemeScope theme="vela" scheme="dark" locale="en-IN">
  <SyntaraScreen
    document={json}
    onAction={(action, { nodeType, nodeId }) => route(action)}
    onIssue={(issue) => log(issue.code, issue.path, issue.message)}
    fallback={<UpdateTheApp />}
  />
</ThemeScope>

The rules

Each rule is written out in full in the package README.

  • Data, not code. A node is a type, props, text, slots and an action. Functions, styles, class names and markup never cross the wire. A Button gets an action instead of onPress, and strings are always drawn as text. Props come from meta.
  • Two kinds of action. navigate to an https:// URL or an app path, or an event with a name and a flat payload for the host to handle. Anything else, including javascript:, is rejected. Actions.
  • No brand in a screen. A document has no theme, tenant, brand, scheme or density. The client that draws it decides those, so one document works in every tenant. No brand in a screen.
  • Accessibility is in the schema. A Button needs text or an ariaLabel. A Link needs text. A ProgressBar or Meter needs a label. Badge, Tag and Alert need words, so status is never colour alone. A document that breaks one of these is invalid. Accessibility is part of the contract.
  • Versions. schemaVersion is semver, and a client supports one major. Minor versions only add. Removing or renaming anything is a major. Versions.
  • Unknowns. A client ignores unknown props and fields, drops unknown values so the default applies, and draws a node’s fallback for an unknown type. It never draws raw JSON. Unknowns.
  • Deprecations. A prop or value that was deprecated before this major of the schema never enters it. One deprecated inside a major stays until the next major and is reported when used. That’s why Button’s variant: "danger" isn’t in the schema. How a deprecation reaches the schema, and Governance.

No node takes these props:

PropWhy not
classNameClass names are the web client's styling hook, and a screen can't know them. Use variant, tone and size, which map to tokens.
styleRaw CSS would bypass the tokens. Use variant, tone and size; layout gaps are token names on Stack and Inline.
keyReact's list key. The node's id does that job on the wire.
refA handle to a DOM element on the client. Nothing on the wire can hold one.

What’s in the slice

Schema 1.0.0 has 26 node types: 22 drawn by 15 of the 53 Syntara components, and 4 of the schema’s own for layout and text. By maturity: 21 beta and 5 alpha. A node from a component takes the component’s maturity; the schema’s own nodes are alpha. Icons are referenced by name, and 243 icons from @syntara/icons are on the list.

NodeDrawn byMaturityAccessibility rules
ButtonButtonbetabutton-namebutton-icon-size-name
LinkLinkbetalink-text
BadgeBadgebetabadge-text
TagTagbetatag-text
AlertAlertbetaalert-text
CardCardbetaNone
CardHeaderCardbetaNone
CardTitleCardbetacard-title-text
CardDescriptionCardbetaNone
CardActionCardbetaNone
CardContentCardbetaNone
CardFooterCardbetaNone
StatTileStat Tilebetastat-tile-text
StatTileGroupStat TilebetaNone
AvatarAvatarbetaavatar-name
SeparatorSeparatoralphaNone
ProgressBarProgress Barbetaprogress-name
MeterMeterbetameter-name
EmptyStateEmpty Statebetaempty-state-title
EyebrowEyebrowbetaeyebrow-text
AmountAmountbetaNone
IconTileIcon Tilebetaicon-tile-image-alt
StackThe schemaalphaNone
InlineThe schemaalphaNone
TextThe schemaalphaNone
HeadingThe schemaalphaheading-text

What’s out

Inputs, overlays, tables and charts. They hold state (a value, an open menu, a sort order) and send events back as it changes. That needs a fuller action model than navigate and event: state bindings, validation and submission. This slice doesn't define one.

38 of 53 components aren’t in the slice. From the components that are in it, these parts stay out:

  • CardMedia: Holds an image, video or device mock. The slice has no image or media node yet, and the glow behind it is proven for media only.
  • AvatarGroup: Its '+N' label is a function (moreLabel) whose default is English only, so a screen in another language would announce English. Send Avatars in an Inline.

Native tokens

The token build (pnpm tokens) also writes two files per tenant: SyntaraTokens.kt for Jetpack Compose and SyntaraTokens.swift for SwiftUI. They hold the same colours, spacing, radii, type and motion as the CSS, light and dark. The build reads the colours back out of each file and checks every contrast pair on them. On this page, the same check runs again when the site is built:

TenantCompose (SyntaraTokens.kt)SwiftUI (SyntaraTokens.swift)
Vela118 of 118 pass118 of 118 pass
Harbor118 of 118 pass118 of 118 pass
Qamar118 of 118 pass118 of 118 pass
Care118 of 118 pass118 of 118 pass
Haat118 of 118 pass118 of 118 pass
House (this site)118 of 118 pass118 of 118 pass

What that does and doesn’t prove:

  • The Swift files were type-checked with swiftc against the macOS SDK. They have not been built for iOS.
  • The Kotlin files have not been compiled. No Kotlin compiler was installed, so tests check their structure instead.
  • No native components exist. These files give an app team Syntara’s tokens, not its buttons.

The full notes, including units, touch targets and what CSS has that native doesn’t, are in README-native.md.

What this is not

A contract and one web renderer

Not a server-driven UI framework: no fetching, caching, state or analytics. No native renderer: <SyntaraScreen> is web only, and an Android or iOS client would have to be written against the schema. And not every component: inputs, overlays, tables and charts aren’t in the slice.