Custom screens
Your users should see your product, not a generic admin panel. Your agent can write your own React screens, place them on any page, let them take over the whole screen, set your brand colour and replace the sign-in page. They still read and write through the same server, so your access rules decide what each user can do.
When to use it
The built-in blocks (lists, forms, stats, boards) get you a working app fast. Write a component when the app needs its own look or interaction: a class schedule, a swipe deck, a kitchen display, a dashboard with charts.
Step by step
- Write the component: one TSX file with one default export.
- Gate it:
check_component(MCP) orPOST /api/dev/v1/components/checkruns every gate in seconds. - Declare it in
componentswith its source, and place it on a page with acustomlayout node. - Apply, then
screenshotthe page as each role and fix what you see.
Code
"components": [{
"name": "ClassSchedule",
"kind": "code",
"purpose": "Member schedule: pick a day, book a class",
"props": { "title": { "type": "text" } },
"sample": { "title": "This week at Stillwater" },
"fallback": { "EmptyState": { "title": "The schedule is taking a breather" } },
"source": "<the whole TSX file>"
}],
"pages": [{
"name": "schedule", "route": "/schedule", "title": "Schedule", "personas": ["member"],
"layout": { "kind": "custom", "custom": "ClassSchedule", "props": { "title": "This week at Stillwater" } }
}]The component receives its props plus app, your handle to the app's data:
import { useEffect } from "react";
import type { AppHandle } from "@app/kit/primitives";
type ClassScheduleProps = { title?: string; app?: AppHandle };
export default function ClassSchedule({ title = "This week", app }: ClassScheduleProps) {
useEffect(() => (app ? app.live("session") : undefined), [app]); // realtime
const sessions = app?.rows("session") ?? []; // guard: empty in the gate
return (
<ul className="space-y-3">
{sessions.length === 0 && <li className="text-muted-foreground">No classes yet.</li>}
{sessions.map((s) => (
<li key={String(s.id)} className="rounded-2xl border border-border bg-card p-4">{String(s.title)}</li>
))}
</ul>
);
}The three full components of the booking app are in the tutorial. Every member of app is in the SDK reference.
What a component can use
| Import | For |
|---|---|
react | Hooks: useState, useEffect, useMemo and the rest |
@app/kit/primitives | Layout and content primitives, MapView, Embed, RichTextEditor, Markdown, and type AppHandle |
recharts | Line, bar, area, pie, radar and scatter charts |
motion/react | Animation, enter and leave, drag, reorder |
lucide-react | Icons from the icon set |
qrcode.react | QR codes for tickets and check-in |
import { ResponsiveContainer, BarChart, Bar, XAxis } from "recharts";
import { MapView, Embed, RichTextEditor, Markdown } from "@app/kit/primitives";
<div style={{ height: 220 }}>
<ResponsiveContainer width="100%" height="100%">
<BarChart data={rows}><XAxis dataKey="day" /><Bar dataKey="booked" fill="hsl(var(--primary))" /></BarChart>
</ResponsiveContainer>
</div>
<MapView markers={[{ lat: 38.7223, lng: -9.1393, label: "Stillwater" }]} height="md" />
<Embed url="https://youtu.be/…" title="Studio tour" aspect="16:9" />
<form onSubmit={(e) => { e.preventDefault(); void app.update("post", id, { body: md }); }}>
<RichTextEditor label="Body" value={md} onChange={setMd} />
<button type="submit">Save</button>
</form>
<Markdown text={post.body} />Components can also call server functions (app.call), the app's AI (app.ai), payments (app.pay) and uploads (app.upload).
Full-screen pages
Add "chrome": "none" to a page and your component owns the whole screen, with its own header and tab bar. Navigate with app.navigate("/my-classes") and add your own sign-out (app.auth.signOut()).
Brand colour
"app": { "name": "Stillwater", "brand_color": "#2F7D6D" }The exact colour becomes the primary colour on buttons, links, focus rings and the platform sign-in screen, in light and dark mode. Text on it is near-black or white, whichever reads better. Change it with { "op": "set_app", "brand_color": "#2F7D6D" }.
Custom sign-in screen
Write a component and name it in "auth": { "screen": "StudioLogin" }. It replaces the platform screen and gets app.auth: signIn, signUp, requestReset, mode and setMode, canSignUp, providers and startProvider, skip. If it throws, the platform screen shows instead, so users are never locked out.
Gotchas
- Components are sandboxed. No fetch, storage, cookies, eval, iframes or
dangerouslySetInnerHTML. Use a server function for network calls andEmbedfor frames. - Render for empty data. The gate renders with the sample props and with none; guard every array and show an empty state.
- No
Math.randomorDate.nowduring render. Read the time in an effect, as the booking app does. - Forms need
onSubmitand noaction,methodortarget. - Up to 40 KB per component. Split big screens.
- Use theme tokens (
bg-primary,bg-card,text-muted-foreground) so the brand colour and dark mode follow. - Add a
fallback(a primitive tree) so a failed component shows something useful. - Change part of a component with
{ "op": "edit_component", "name": "ClassSchedule", "edits": [{ "find": "...", "replace": "..." }] }.