# Introduction
Before you start using syntux, it is helpful to understand the problem it's trying to solve.
What is generative UI? [#what-is-generative-ui]
There are three layers of personalization for websites; **function**, **content** and **interface**.
Your bank dashboard is a **function** personalized website. Your social media feed is a **content** personalized interface.
For the longest time, the **interface** layer was untouched. syntux solves this.
What is syntux? [#what-is-syntux]
syntux is a user-interface generator, designed specifically to **display data**.
What could this data be? Anything! It could be analytics, a social media feed, or be an empty object.
```jsx
const valueToDisplay = {
"username": "John",
"email": "john@gmail.com",
"age": 22
}
```
Do not let the fact that it is designed to display data intimidate you - that is simply a testament to how token-efficient it is.
For instance, if you provide an array `value` with 10,000 items, it will cost you the same as one with 10 items. That is how efficient syntux is.
Ready to get started? **It takes 5 minutes.**
# Installation
import { NextJsIcon, ReactRouterIcon, AstroIcon, SvelteIcon } from '@/components/ui/icons';
import Link from 'next/link';
syntux is designed from the ground up for React.
Choose Your Framework [#choose-your-framework]
Next.js
recommended
React Router
Astro
Svelte
coming soon!
# Next.js
Initialize [#initialize]
In the root of your project, run:
```bash
npx @getsyntux/cli@latest
```
This will install the system prompt in the `@/lib/getsyntux` folder.
By default, the prompt is installed in the `@/lib/getsyntux/spec.ts` file. Edit it directly!
Create render endpoint [#create-render-endpoint]
Use Vercel's [AI SDK](https://github.com/vercel/ai) to select your model of choice.
Then, use `createSyntuxHandler` to handle the request inside your endpoint:
```js
// app/api/syntux/route.ts
import { createAnthropic } from '@ai-sdk/anthropic';
import { createSyntuxHandler } from 'getsyntux/server';
import spec from '@/lib/getsyntux/spec';
const anthropic = createAnthropic({
apiKey: "..."
})
export async function POST(request: Request){
const handler = createSyntuxHandler({
model: anthropic('claude-haiku-4-5'),
spec
});
// custom logic here... (authentication)
return handler(request);
}
```
Create update endpoint [#create-update-endpoint]
This is **optional**, only if you wish to support [reactivity](/usage/reactivity).
The flow is the same, but with a different handler.
Define a separate endpoint:
```js
// app/api/syntux/rerender/route.ts
import { createAnthropic } from '@ai-sdk/anthropic';
import { createSyntuxRerenderHandler } from 'getsyntux/server';
import spec from '@/lib/getsyntux/spec';
const anthropic = createAnthropic({
apiKey: "..."
})
export async function POST(request: Request){
const handler = createSyntuxRerenderHandler({
model: anthropic('claude-haiku-4-5'),
spec
});
// custom logic here... (authentication)
return handler(request);
}
```
Usage [#usage]
Provide the rerender (and rerender, if used) endpoint URLs to the `endpoint` and `rerenderEndpoint?` props:
```jsx
// app/dashboard/page.tsx
import { GeneratedUI } from 'getsyntux/client';
export default function Page() {
const value = { username: 'John', email: 'john@gmail.com', age: 22 };
return (
);
}
```
# React Router
Initialize [#initialize]
In the root of your project, run:
```bash
npx @getsyntux/cli@latest
```
This will install the system prompt in the `@/lib/getsyntux` folder.
By default, the prompt is installed in the `@/lib/getsyntux/spec.ts` file. Edit it directly!
Create render endpoint [#create-render-endpoint]
Use Vercel's [AI SDK](https://github.com/vercel/ai) to select your model of choice.
Then, use `createSyntuxHandler` to handle the request inside your endpoint:
```js
// app/routes/api.syntux.ts
import { createAnthropic } from '@ai-sdk/anthropic';
import { createSyntuxHandler } from 'getsyntux/server';
import spec from '@/lib/getsyntux/spec';
import type { ActionFunctionArgs } from '@remix-run/node';
const anthropic = createAnthropic({
apiKey: "..."
})
export async function action({ request }: ActionFunctionArgs) {
const handler = createSyntuxHandler({
model: anthropic('claude-haiku-4-5'),
spec
});
// custom logic here... (authentication)
return handler(request);
}
```
Create update endpoint [#create-update-endpoint]
This is **optional**, only if you wish to support [reactivity](/usage/reactivity).
The flow is the same, but with a different handler.
Define a separate endpoint:
```js
// app/routes/api.syntux.rerender.ts
import { createAnthropic } from '@ai-sdk/anthropic';
import { createSyntuxHandler } from 'getsyntux/server';
import spec from '@/lib/getsyntux/spec';
import type { ActionFunctionArgs } from '@remix-run/node';
const anthropic = createAnthropic({
apiKey: "..."
})
export async function action({ request }: ActionFunctionArgs) {
const handler = createSyntuxRerenderHandler({
model: anthropic('claude-haiku-4-5'),
spec
});
// custom logic here... (authentication)
return handler(request);
}
```
Usage [#usage]
Provide the rerender (and rerender, if used) endpoint URLs to the `endpoint` and `rerenderEndpoint?` props:
```jsx
// app/routes/dashboard.tsx
import { GeneratedUI } from 'getsyntux/client';
import { useLoaderData } from '@remix-run/react';
export async function loader() {
return { username: 'John', email: 'john@gmail.com', age: 22 };
}
export default function Dashboard() {
const value = useLoaderData();
return (
);
}
```
# Astro
Make sure the React integration is installed:
```bash
npx astro add react
```
Initialize [#initialize]
In the root of your project, run:
```bash
npx @getsyntux/cli@latest
```
This will install the system prompt in the `@/lib/getsyntux` folder.
By default, the prompt is installed in the `@/lib/getsyntux/spec.ts` file. Edit it directly!
Create render endpoint [#create-render-endpoint]
Use Vercel's [AI SDK](https://github.com/vercel/ai) to select your model of choice.
Then, use `createSyntuxHandler` to handle the request inside your endpoint:
```js
// src/pages/api/syntux.ts
import { createAnthropic } from '@ai-sdk/anthropic';
import { createSyntuxHandler } from 'getsyntux/server';
import spec from '@/lib/getsyntux/spec';
import type { APIRoute } from 'astro';
const anthropic = createAnthropic({
apiKey: "..."
})
export const POST: APIRoute = async ({ request }) => {
const handler = createSyntuxHandler({
model: anthropic('claude-haiku-4-5'),
spec
});
// custom logic here... (authentication)
return handler(request);
}
```
Create update endpoint [#create-update-endpoint]
This is **optional**, only if you wish to support [reactivity](/usage/reactivity).
The flow is the same, but with a different handler.
Define a separate endpoint:
```js
// src/pages/api/syntux/rerender.ts
import { createAnthropic } from '@ai-sdk/anthropic';
import { createSyntuxHandler } from 'getsyntux/server';
import spec from '@/lib/getsyntux/spec';
import type { ActionFunctionArgs } from '@remix-run/node';
const anthropic = createAnthropic({
apiKey: "..."
})
export const POST: APIRoute = async ({ request }) => {
const handler = createSyntuxRerenderHandler({
model: anthropic('claude-haiku-4-5'),
spec
});
// custom logic here... (authentication)
return handler(request);
}
```
Usage [#usage]
Provide the rerender (and rerender, if used) endpoint URLs to the `endpoint` and `rerenderEndpoint?` props:
```jsx
---
// src/pages/dashboard.astro
const value = { username: 'John', email: 'john@gmail.com', age: 22 };
---
```
# Getting started
One component is all you need:
```jsx
import { GeneratedUI } from 'getsyntux/client';
```
If you are passing a **large array** to `value`, use the [`skeletonize` property](/usage/advanced#skeletonize-property) to avoid high token costs.
See the following examples to understand its capabilities.
Examples [#examples]
Basic example [#basic-example]
Generate a simple UI with a hint:
```jsx
import { GeneratedUI } from 'getsyntux/client';
export default function Page() {
const value = { username: 'John', email: 'john@gmail.com', age: 22 };
return (
);
}
```
```jsx
import { GeneratedUI } from 'getsyntux/client';
import { useLoaderData } from '@remix-run/react';
export async function loader() {
return { username: 'John', email: 'john@gmail.com', age: 22 };
}
export default function Dashboard() {
const value = useLoaderData();
return (
);
}
```
```js
---
const value = { username: 'John', email: 'john@gmail.com', age: 22 };
---
```
No. It is readable by the client.
Prompts you wish to be secret should be implemented at the endpoint.
Caching [#caching]
Caching should be implemented on the controller side, then retrieved and passed to the client.
Use `onGenerate` to capture the schema.
In this example, we are using `globalThis` to store the cache in memory. **This is only for demonstration!** Use a real database in production.
```js
const globalWithCache = globalThis as unknown as {
syntuxCache: Map | undefined;
};
export const cache = globalWithCache.syntuxCache ?? new Map(); // user id → schema
if(process.env.NODE_ENV !== 'production'){
globalWithCache.syntuxCache = cache;
}
export async function POST(request: Request){
const userId = 10;
const handler = createSyntuxHandler({
model: anthropic('claude-sonnet-4-5'),
spec,
onGenerate: (schema) => cache.set(userId, schema)
});
// custom logic here... (authentication)
return handler(request);
}
```
```js
const globalWithCache = globalThis as unknown as {
syntuxCache: Map | undefined;
};
export const cache = globalWithCache.syntuxCache ?? new Map(); // user id → schema
if(process.env.NODE_ENV !== 'production'){
globalWithCache.syntuxCache = cache;
}
export async function action({ request }: ActionFunctionArgs) {
const userId = 10;
const handler = createSyntuxHandler({
model: anthropic('claude-sonnet-4-5'),
spec,
onGenerate: (schema) => cache.set(userId, schema)
});
// custom logic here... (authentication)
return handler(request);
}
```
```js
const globalWithCache = globalThis as unknown as {
syntuxCache: Map | undefined;
};
export const cache = globalWithCache.syntuxCache ?? new Map(); // user id → schema
if(process.env.NODE_ENV !== 'production'){
globalWithCache.syntuxCache = cache;
}
export const POST: APIRoute = async ({ request }) => {
const userId = 10;
const handler = createSyntuxHandler({
model: anthropic('claude-sonnet-4-5'),
spec,
onGenerate: (schema) => cache.set(userId, schema)
});
// custom logic here... (authentication)
return handler(request);
}
```
Then, provide the cached value to the `cached` property:
```jsx
import { GeneratedUI } from 'getsyntux/client';
import { cache } from './api/syntux/route';
export default function Page() {
const userId = 10;
const value = { username: 'John', email: 'john@gmail.com', age: 22 };
return (
);
}
```
```jsx
import { GeneratedUI } from 'getsyntux/client';
import { useLoaderData } from '@remix-run/react';
import { cache } from './api.syntux';
export async function loader() {
const userId = 10;
return {
cached: cache.get(userId),
value: { username: 'John', email: 'john@gmail.com', age: 22 },
};
}
export default function Dashboard() {
const { cached, value } = useLoaderData();
return (
);
}
```
```js
---
import { Dashboard } from '../components/Dashboard';
import { cache } from './api/syntux';
const userId = 10;
const value = { username: 'John', email: 'john@gmail.com', age: 22 };
---
```
Custom components [#custom-components]
Use custom React components, either your own or somebody else's (a UI library):
```jsx
import { GeneratedUI } from 'getsyntux/client';
import { Card, Avatar } from '@/components/ui';
export default function Page() {
const value = { username: 'John', email: 'john@gmail.com', avatar: '/john.png' };
return (
);
}
```
```jsx
import { GeneratedUI } from 'getsyntux/client';
import { Card, Avatar } from '~/components/ui';
import { useLoaderData } from '@remix-run/react';
export default function Dashboard() {
const value = useLoaderData();
return (
);
}
```
```js
---
const value = { username: 'John', email: 'john@gmail.com', avatar: '/john.png' };
---
```
Make sure components are marked with `"use client"`.
The `components` array can be automatically generated using the [`generate-defs` CLI command](/api#generate-defs).
Customize animation [#customize-animation]
By default, new elements fade in from below when mounted.
This motion cannot yet be customized. However, the duration and offset can, using the `animate` property:
```jsx
```
In order to disable the animation, set `offset` to 0 and/or `duration` to 0.
By default, syntux is conservative when applying animations in order to not break the tree layout.
Animations are applied through the `style` property, via the `opacity`, `transform`, `transition` and `willChange` attributes.
**The animation may not work for a custom component**, but this can be fixed!
If the animation isn't displaying, that probably means your component doesn't respond to the `style` property. You need to include `style` as a property.
For instance:
```jsx
export function CustomComponent({ ...rest }){ /* <-- rest takes care of style */
return
{/* original code here */}
}
```
# Reactivity
The [`useSyntux` hook](/api#usesyntux) allows you to modify the user interface after it has been generated.
Change the value (static) [#change-the-value-static]
If you simply want to update the `value` to display, and keep the user interface.
The following example adds an item to the `value` array with `setValue`:
```jsx
"use client";
import { useSyntux } from 'getsyntux/client';
export default function CustomComponent() {
const { value, setValue } = useSyntux();
return (
);
}
```
Regenerate the UI (dynamic) [#regenerate-the-ui-dynamic]
First, ensure you have exposed an update endpoint during [installation](/installation).
Assign rerenderEndpoint [#assign-rerenderendpoint]
Provide the URL of the rerender endpoint through the `rerenderEndpoint` prop:
```jsx
```
Call setValue [#call-setvalue]
Finally, in order to trigger a regeneration, provide `options` to `setValue` like so:
```jsx
"use client";
export default function CustomComponent() {
const { value, setValue } = useSyntux();
return (
);
}
```
A new UI will then be streamed when the button is clicked.
# Advanced usage
syntux is **highly optimized** to save tokens. To understand how, we must first understand how syntux works.
How it works [#how-it-works]
Generated interfaces must be *secure*, *reusable* and *cacheable*.
As such, syntux does not:
* generate code (HTML/JSX), or...
* hardcode the `value`
Instead, syntux generates a JSON-DSL representation of the UI, known as the React Interface Schema (RIS).
The RIS **does not hardcode values**. It **binds** to properties of the `value` and has **built-in iterators**, making it reusable and token-efficient for arrays.
An example of the RIS:
```json
{"id":"loop_1", "parentId":"root", "type":"__ForEach__", "props":{"source":"authors"}}
{"id":"card_1", "parentId":"loop_1", "type":"div", "props":{"className":"card"}, "content": {"$bind": "$item.name"}}
```
skeletonize property [#skeletonize-property]
When you send a generate request, syntux will stringify the `value` in its entirety and send it to the LLM.
This is OK for most applications, but not for arrays of large sizes. Doing so would consume a lot of tokens.
In order to solve this problem, you must **generate a skeleton** of the input with the `skeletonize` parameter:
```jsx
const myArrayToDisplay = [ ... 1000 items ... ];
```
The `skeletonize` prop works by removing all property values. It only keeps the **type information** of the object, providing the LLM with just
enough information to bind to properties.
An example input:
```json
{
"user": { "id": 123, "name": "Grant", "verified": true },
"posts": [
{ "title": "Generative UI", "likes": 45, "tags": ["js", "web"] },
{ "title": "Is Awesome!", "likes": 6, "tags": ["ai"] }
],
"settings": {
"theme": "dark",
"notifications": { "email": true, "sms": false }
}
}
```
After skeletonization:
```json
{
"user": { "id": "number", "name": "string", "verified": "boolean" },
"posts": [
{ "title": "string", "likes": "number", "tags": ["string"] }
],
"settings": {
"theme": "string",
"notifications": { "email": "boolean", "sms": "boolean" }
}
}
```
As you can see, quite powerful.
Use skeletonize sparingly!>}>
By stripping out property values, you limit the context and creativity of the model.
In general, only use it for large arrays.
Cost estimation [#cost-estimation]
As a TL;DR of the two points above:
syntux is **very efficient** when it comes to tokens.
**Output tokens**:
* it **does not hardcode** values. Instead, it **binds** to properties of the `value`.
* it has **built-in** iterators to avoid repetition, handling large arrays with ease.
**Input tokens**:
* with skeletonization (see above), it can reduce any input to a trivial size.
Cost estimation table: [#cost-estimation-table]
| Provider | Model | Generations per $5 | Recommended? |
| --------- | ------------------ | ------------------ | ------------ |
| Anthropic | Opus 4.5 | 1,000 | ❌ overkill |
| Anthropic | Sonnet 4.5 | 1,667 | ❌ overkill |
| OpenAI | GPT 5.2 | 2,860 | ❌ overkill |
| OpenAI | GPT 5.2 Mini | 20,000 | ✓ good |
| Google | Gemini Pro Preview | 2,500 | ❌ overkill |
| Google | Gemini Flash Flash | 10,000 | ✓ good |
Assuming you have 20,000 DAUs, regenerating the UI once every week, you would incur a cost of $15 per month. At that scale, the cost is negligible.
The system prompt is 1k tokens (1.02k to be exact).
You may find that the cost is even less, as some providers (OpenAI, Google, Anthropic) will cache the system prompt.
# API
Components [#components]
GeneratedUI [#generatedui]
```jsx
Awaiting stream...} /* optional */
errorFallback={
An error occurred.
} /* optional */
cached={cacheStr} /* optional */
onGenerate={(str) => { console.log(str) }} /* optional */
onUpdate={(str) => { console.log(str) }} /* optional */
animate={{ offset: 20, duration: 200 }} /* optional */
skeletonize={false} /* optional */
/>
```
Explanation:
* `value` (`any`): the value (object, primitive, or array) to be displayed.
* `endpoint` (`string`): the relative URL endpoint created with createSyntuxHandler.
* `rerenderEndpoint` (`string`): the relative URL endpoint for regeneration.
* `hint?` (`string`): custom instructions for the LLM.
* `components?` (`object[]`): list of allowed components that the LLM can use.
* `placeholder?` (`JSX.Element`): element to be displayed whilst awaiting streaming to begin.
The `placeholder` will only display while connecting (when no UI is available yet), **not while it is generating**.
If you want to wait for generation to complete before displaying, wrap the GeneratedUI in a `Suspense`.
* `errorFallback?` (`JSX.Element`): element to be displayed if an error occurs.
* `cached?` (`string`): pre-generated schema string (from onGenerate), skips API call.
* `onGenerate?` (`(arg0: str) => void`): a callback for capturing the schema once generation is complete.
* `onUpdate?` (`(arg0: str) => void`): a callback invoked when the stream updates, containing the latest schema.
* `animate?` (`AnimateOptions`): configuration for on-mount animation; object with `offset` and `duration`.
* `skeletonize?` (`boolean`): compresses value for large inputs (arrays) or untrusted input. See the [full explanation](/usage/advanced#skeletonize-property).
Methods [#methods]
useSyntux() [#usesyntux]
A hook for retrieving, modifying the `value` and regenerating the UI.
```ts
const { value, setValue } = useSyntux();
```
`value` is identical to the object provided to the `value` prop of `GeneratedUI`.
setValue [#setvalue]
Method for updating the `value` and/or regenerating the UI.
```ts
setValue(value: any, options?: RerenderOptions): Promise | null
```
* `value`: the `value` to be displayed (for [static updates](/reactivity#change-the-value-static))
* `options?`: options for [dynamic regeneration](/reactivity#regenerate-the-ui-dynamic)
If `options` is not provided, it will be a static update.
The `options` must adhere to this format:
```js
{
regenerate: true, // if false, treated as static update
hint: "The new UI should look like..."
}
```
A `Promise` is returned if it is a regeneration request, which will resolve once the new stream has initiated.
CLI [#cli]
generate-defs [#generate-defs]
Automatically generate component definitions, which you can paste directly into the `components` property.
```bash
npx @getsyntux/cli generate-defs ./path/to/component.tsx
```
It will look something like this:
```bash
npx @getsyntux/cli generate-defs .\lib\Button.tsx
getsyntux: parsing file...
getsyntux: found 1 component(s) with prop definitions [Button]
getsyntux: component #1: Button
√ What does this component do? (optional) ... displays an interactive button
getsyntux: generated definitions (safe to copy & paste directly):
{ name: "Button", props: "{ text: string, disabled: boolean }", component: Button, context: "displays an interactive button" },
```