Components
Define the UI contract TailorKit apps can render inside your host product.
Components are the UI building blocks that TailorKit apps are allowed to use. An app does not import your React components directly and it does not render DOM inside your page. Instead, it renders named TailorKit components from the contract your host product exposes.
The host owns the real component implementation. TailorKit apps only see the component name, its serializable fields, its callbacks, and its slots.
This keeps app UI consistent with your product while keeping the host in control of what can be rendered.
The Component Contract
A component definition has three parts:
fields: serializable props the app can pass to the component.callbacks: functions the app can receive when host UI events happen.slots: named children areas the app can fill.
The TanStack Start example defines shared components in
@examples/shared and passes them to createTailorKit.
import type { Component } from "tailorkit";
import { primitives } from "tailorkit/zod";
import { z } from "zod";
const Button = {
fields: z.object({
size: z.enum(["default", "sm", "lg", "icon", "icon-sm", "icon-lg"]).optional(),
variant: z.enum(["default", "secondary", "ghost", "outline", "destructive"]).optional(),
}),
callbacks: {
onClick: {},
},
slots: ["default"],
} as const satisfies Component;
const Input = {
fields: z.object({
value: z.string(),
}),
callbacks: {
onValueChange: {
input: z.object({ value: z.string() }),
},
},
slots: ["default"],
} as const satisfies Component;
export const components = {
...primitives(primitiveTheme),
Button,
Input,
};as const satisfies Component keeps the literal component shape available to
TypeScript while checking that the object is a valid TailorKit component.
Fields
Fields are the serializable props an app can set. In the example above, apps can
pass variant and size to Button, but they cannot pass arbitrary React
props.
Fields use the same schema model as screens and actions. The example uses Zod
via tailorkit/zod, and TailorKit serializes the schema into JSON Schema for
the runtime contract.
const TabsTab = {
fields: z.object({
value: z.string(),
}),
slots: ["default"],
} as const satisfies Component;Fields should describe stable product capabilities, not incidental React
implementation details. Prefer variant: "secondary" over exposing class names
or internal state that may change when you restyle the component.
Callbacks
Callbacks are UI events that flow from the host-rendered component back to the TailorKit app. They are not server actions and they are not trusted work. Use callbacks for local interaction such as clicks, selected tab changes, and input changes.
const Tabs = {
fields: z.object({
value: z.string(),
}),
callbacks: {
onValueChange: {
input: z.object({ value: z.string() }),
},
},
slots: ["default"],
} as const satisfies Component;A callback can define input and output schemas. The React renderer decides
when to call the callback and what payload to send.
Input: ({ props: { onValueChange, ...rest }, slots }) => (
<Input onChange={(event) => onValueChange({ value: event.target.value })} {...rest}>
{slots.default}
</Input>
),Use Actions for trusted server-side work such as database writes, permission checks, and external API calls.
Slots
Slots define where child UI can be placed. Most simple components use a
default slot.
const TabsList = {
slots: ["default"],
} as const satisfies Component;On the host, the React renderer receives slots as React nodes.
TabsList: ({ props, slots }) => <TabsList {...props}>{slots.default}</TabsList>,Declare only the slots you intend apps to use. A component without slots is a
leaf component from the app's point of view.
Register Components On The Server
Pass the component map to createTailorKit. This makes the component contract
available from the TailorKit API route.
import { actions, components, screens } from "@examples/shared";
import { createTailorKit } from "tailorkit";
export const tailorKit = createTailorKit({
assetsBaseUrl: process.env.TAILORKIT_ASSETS_BASE_URL ?? "http://localhost:8333/tailorkit",
projectKey: process.env.TAILORKIT_PROJECT_KEY,
components,
screens,
actions,
});The server exposes the serialized contract through the TailorKit handler. The
handler returns schema.serialize() from /api/tailorkit/schema and includes
the same schema in /api/tailorkit/meta.
import { getDemoUserFromRequest } from "@examples/shared";
import { tailorKit } from "#lib/tailorkit";
import { createFileRoute } from "@tanstack/react-router";
const handle = ({ request }: { request: Request }) =>
tailorKit.handler(request, {
authenticate: () => {
const user = getDemoUserFromRequest(request);
if (!user) {
return null;
}
return {
actionContext: { user },
scopeId: user.id,
};
},
});
export const Route = createFileRoute("/api/tailorkit/$")({
server: {
handlers: {
GET: handle,
POST: handle,
},
},
});Register Renderers On The Client
The client maps each server-defined component name to a real React renderer.
createTailorKitClient<typeof tailorKit>() uses the server schema type so
renderer props and slots are typed from your component definitions.
import type { tailorKit } from "./tailorkit";
import { primitiveTheme } from "@examples/shared";
import { primitives as reactPrimitives, createTailorKitClient } from "tailorkit/react";
import { Button } from "@tailorkit/ui/components/button";
import { Input } from "@tailorkit/ui/components/input";
export const tailorKitClient = createTailorKitClient<typeof tailorKit>({
baseUrl:
typeof window === "undefined"
? "http://localhost/api/tailorkit/"
: new URL("/api/tailorkit/", window.location.origin),
theme: primitiveTheme,
components: {
...reactPrimitives,
Button: ({ props, slots }) => <Button {...props}>{slots.default}</Button>,
Input: ({ props: { onValueChange, ...rest }, slots }) => (
<Input onChange={(event) => onValueChange({ value: event.target.value })} {...rest}>
{slots.default}
</Input>
),
},
});When a remote app renders Button, TailorKit looks up the registered host
renderer by name and renders the real @tailorkit/ui button. If a remote app
uses a component that is not registered, the host throws:
TailorKit component "Name" is not registered.
The Serialized Schema
TailorKit serializes components into a stable runtime shape:
{
version: 1,
components: {
Button: {
fields: { /* JSON Schema */ },
callbacks: {
onClick: {
input: undefined,
output: undefined,
},
},
slots: ["default"],
},
},
screens: {},
actions: {},
}This schema is the contract between the host product, generated app bindings, and deployed TailorKit apps. Changing it is a compatibility decision, not just a local refactor.
Keep Components Stable
Renaming a component, removing a field, changing a field's type, removing a callback, or removing a slot can break existing apps. Add new optional fields, new callback payload fields, or new components when possible. When you need a breaking change, version the component name or coordinate the app update.
Naming And Compatibility
Component names are public API. TailorKit also derives DOM-style tag names from
them for remote rendering, so TextArea can be addressed as tailorkit-text-area
inside the runtime.
Use product-level names that can survive implementation changes:
- Good:
Button,CustomerCard,AssigneeSelect. - Risky:
NewButtonV2,RadixSelectTrigger,BluePill.
Use fields for stable variants and behavior, callbacks for app-handled UI events, slots for child composition, and actions for trusted server-side work.