` with ARIA tie | Yes |
## Examples
### Disabled
```tsx
import {
Checkbox,
CheckboxControl,
CheckboxIndicator,
} from "@/components/ui/checkbox";
import { Fieldset, FieldsetLegend } from "@/components/ui/fieldset";
export default function FieldsetDisabled() {
return (
Notifications (disabled)
Email notifications
SMS notifications
);
}
```
### With Fields
```tsx
import { Field, FieldLabel, FieldDescription } from "@/components/ui/field";
import { Fieldset, FieldsetLegend } from "@/components/ui/fieldset";
import { Input } from "@/components/ui/input";
export default function FieldsetWithFields() {
return (
Shipping Address
Street
City
ZIP Code
5-digit ZIP code.
);
}
```
## Styling
### Data Slots
Use `data-slot` attributes to target specific parts of the fieldset:
| Slot name | Element |
| ----------------- | ----------------------- |
| `fieldset` | Root fieldset container |
| `fieldset-legend` | Legend text |
### Customization Examples
```css
/* Tighter fieldset spacing */
[data-slot="fieldset"] {
@apply gap-4;
}
/* Style the legend as a smaller label */
[data-slot="fieldset-legend"] {
@apply text-sm text-muted-foreground;
}
```
```tsx
{
/* Label-sized legend variant */
}
Settings
{/* ... */}
;
```
## API Reference
### Fieldset
Root container that groups related form controls. Renders a `
` element.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `disabled` | `boolean` | `false` | Whether all controls within the fieldset are disabled |
| `className` | `string` | - | Additional CSS classes |
All [Base UI Fieldset.Root props](https://base-ui.com/react/components/fieldset) are forwarded via `...props`.
### FieldsetLegend
Accessible legend that is automatically associated with the fieldset. Renders a `` element.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `variant` | `"legend" \| "label"` | `"legend"` | Size variant — "legend" for section headings, "label" for smaller inline labels |
| `className` | `string` | - | Additional CSS classes |
All [Base UI Fieldset.Legend props](https://base-ui.com/react/components/fieldset) are forwarded via `...props`.
## Accessibility
### ARIA Attributes
- `Fieldset` renders a `
` element with native grouping semantics.
- `FieldsetLegend` renders a `` but is automatically associated with the fieldset via `aria-labelledby`.
- When `disabled` is set on the fieldset, all nested controls inherit the disabled state.
- Screen readers announce the legend as the group label when navigating into the fieldset.
# Form
> form with consolidated error handling on Base UI
URL: https://prototyper-ui.com/docs/components/form
Base UI reference: https://base-ui.com/react/components/form
```tsx
"use client";
import { Field, FieldLabel, FieldDescription } from "@/components/ui/field";
import { Form } from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
export default function FormDemo() {
return (
);
}
```
## Installation
```bash
pnpm dlx shadcn@latest add https://prototyper-ui.com/r/form.json
```
This will add the following files to your project:
- `components/ui/form.tsx`
## Usage
```tsx
import { Form } from "@/components/ui/form";
;
```
## Examples
### Server Errors
```tsx
"use client";
import { useState } from "react";
import { Field, FieldLabel, FieldError } from "@/components/ui/field";
import { Form } from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
export default function FormServerErrors() {
const [errors, setErrors] = useState>({});
return (
);
}
```
### Validation Modes
```tsx
"use client";
import { Field, FieldLabel, FieldError } from "@/components/ui/field";
import { Form } from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
export default function FormValidationModes() {
return (
);
}
```
## Styling
### Data Slots
Use `data-slot` attributes to target the form:
| Slot name | Element |
| --------- | ----------------- |
| `form` | Root form element |
### Customization Examples
```css
/* Custom form layout */
[data-slot="form"] {
@apply gap-4;
}
```
```tsx
{
/* Override gap spacing */
}
;
```
## API Reference
### Form
Root form element with consolidated error handling. Renders a `
# FormField
> A react-hook-form helper that wires Field, label, description, and error to a Controller in one component
URL: https://prototyper-ui.com/docs/components/form-field
```tsx
"use client";
import { useForm, FormProvider } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { FormField } from "@/components/ui/form-field";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { Button } from "@/components/ui/button";
const schema = z.object({
name: z.string().min(1, "Name is required"),
email: z.string().email("Please enter a valid email"),
message: z.string().min(10, "Message must be at least 10 characters"),
});
type FormValues = z.infer;
export default function FormFieldDemo() {
const form = useForm({
resolver: zodResolver(schema),
defaultValues: { name: "", email: "", message: "" },
});
const onSubmit = (data: FormValues) => {
alert(JSON.stringify(data, null, 2));
};
return (
Submit
);
}
```
## Installation
```bash
pnpm dlx shadcn@latest add https://prototyper-ui.com/r/form-field.json
```
This will add the following files to your project:
- `components/ui/form-field.tsx`
> **Peer dependency:** This component requires `react-hook-form`. Install it alongside:
>
> ```bash
> npm install react-hook-form @hookform/resolvers zod
> ```
> **Note:** This component depends on [Field](/docs/components/field). It will be installed automatically.
## Usage
```tsx
import { useForm, FormProvider } from "react-hook-form"
import { FormField } from "@/components/ui/form-field"
import { Input } from "@/components/ui/input"
const form = useForm({ defaultValues: { email: "" } })
```
`FormField` uses `Controller` + `useFormContext` internally, so it must be rendered inside a ``. It auto-wires `value`, `onChange`, `onBlur`, and `ref` to the child input via `cloneElement`.
### When to use FormField vs raw Field
| Use `FormField` | Use raw `Field` + `Controller` |
| ------------------------------------- | ------------------------------------------------------- |
| Standard text inputs, textareas | Select, NumberField, Checkbox, Switch, RadioGroup |
| Quick forms with minimal boilerplate | Components that use `onValueChange` / `onCheckedChange` |
| Inputs accepting `value` + `onChange` | Custom controlled components |
For components with non-standard change handlers, use `Controller` directly with `Field` as shown in the [Forms guide](/docs/forms#react-hook-form-integration).
## Examples
### Validation
```tsx
"use client";
import { useForm, FormProvider } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { FormField } from "@/components/ui/form-field";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
const schema = z
.object({
username: z
.string()
.min(3, "Username must be at least 3 characters")
.max(20, "Username must be at most 20 characters")
.regex(
/^[a-z0-9_]+$/,
"Only lowercase letters, numbers, and underscores",
),
email: z.string().email("Please enter a valid email address"),
password: z
.string()
.min(8, "Password must be at least 8 characters")
.regex(/[A-Z]/, "Must contain at least one uppercase letter")
.regex(/[0-9]/, "Must contain at least one number"),
confirmPassword: z.string(),
})
.refine((data) => data.password === data.confirmPassword, {
message: "Passwords do not match",
path: ["confirmPassword"],
});
type FormValues = z.infer;
export default function FormFieldValidation() {
const form = useForm({
resolver: zodResolver(schema),
defaultValues: {
username: "",
email: "",
password: "",
confirmPassword: "",
},
});
const onSubmit = (data: FormValues) => {
alert(JSON.stringify(data, null, 2));
};
return (
Create Account
);
}
```
### All Input Types
```tsx
"use client";
import * as React from "react";
import { useForm, FormProvider, Controller } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { FormField } from "@/components/ui/form-field";
import { Field, FieldLabel, FieldError } from "@/components/ui/field";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import {
Select,
SelectTrigger,
SelectValue,
SelectContent,
SelectItem,
} from "@/components/ui/select";
import {
NumberField,
NumberFieldGroup,
NumberFieldInput,
NumberFieldSteppers,
} from "@/components/ui/number-field";
import {
Checkbox,
CheckboxControl,
CheckboxIndicator,
} from "@/components/ui/checkbox";
import { Switch, SwitchTrack, SwitchThumb } from "@/components/ui/switch";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { Button } from "@/components/ui/button";
const schema = z.object({
name: z.string().min(1, "Name is required"),
bio: z.string().optional(),
role: z.string().min(1, "Please select a role"),
age: z.number().min(18, "Must be at least 18"),
terms: z.boolean().refine((v) => v, "You must accept the terms"),
notifications: z.boolean().optional(),
plan: z.string().min(1, "Please select a plan"),
});
type FormValues = z.infer;
export default function FormFieldAllInputs() {
const form = useForm({
resolver: zodResolver(schema),
defaultValues: {
name: "",
bio: "",
role: "",
age: 25,
terms: false,
notifications: true,
plan: "",
},
});
const onSubmit = (data: FormValues) => {
alert(JSON.stringify(data, null, 2));
};
return (
{/* TextField — works directly with FormField */}
{/* Textarea — works directly with FormField */}
{/* Select — use Controller for onValueChange mapping */}
(
Role
Developer
Designer
Manager
{fieldState.error?.message}
)}
/>
{/* NumberField — use Controller for onValueChange mapping */}
(
Age
{fieldState.error?.message}
)}
/>
{/* Checkbox — use Controller for checked/onCheckedChange */}
(
I accept the terms and conditions
{fieldState.error?.message}
)}
/>
{/* Switch — use Controller for checked/onCheckedChange */}
(
Email Notifications
)}
/>
{/* RadioGroup — use Controller for onValueChange */}
(
Plan
Free
Pro
Enterprise
{fieldState.error?.message}
)}
/>
Submit
);
}
```
### Dynamic Fields
```tsx
"use client";
import { useForm, FormProvider, useFieldArray } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { FormField } from "@/components/ui/form-field";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { Plus, Trash2 } from "lucide-react";
const schema = z.object({
teamName: z.string().min(1, "Team name is required"),
members: z
.array(
z.object({
name: z.string().min(1, "Name is required"),
email: z.string().email("Invalid email"),
}),
)
.min(1, "Add at least one member"),
});
type FormValues = z.infer;
export default function FormFieldDynamic() {
const form = useForm({
resolver: zodResolver(schema),
defaultValues: {
teamName: "",
members: [{ name: "", email: "" }],
},
});
const { fields, append, remove } = useFieldArray({
control: form.control,
name: "members",
});
const onSubmit = (data: FormValues) => {
alert(JSON.stringify(data, null, 2));
};
return (
Team Members
{fields.map((field, index) => (
))}
append({ name: "", email: "" })}
>
Add Member
Create Team
);
}
```
### Multi-Step Form
```tsx
"use client";
import * as React from "react";
import { useForm, FormProvider } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { FormField } from "@/components/ui/form-field";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { Button } from "@/components/ui/button";
const step1Schema = z.object({
firstName: z.string().min(1, "First name is required"),
lastName: z.string().min(1, "Last name is required"),
email: z.string().email("Please enter a valid email"),
});
const step2Schema = z.object({
company: z.string().min(1, "Company is required"),
role: z.string().min(1, "Role is required"),
});
const step3Schema = z.object({
message: z.string().min(10, "Message must be at least 10 characters"),
});
const fullSchema = step1Schema.merge(step2Schema).merge(step3Schema);
type FormValues = z.infer;
const stepSchemas = [step1Schema, step2Schema, step3Schema] as const;
const stepFields: (keyof FormValues)[][] = [
["firstName", "lastName", "email"],
["company", "role"],
["message"],
];
export default function FormFieldMultiStep() {
const [step, setStep] = React.useState(0);
const form = useForm({
resolver: zodResolver(fullSchema),
defaultValues: {
firstName: "",
lastName: "",
email: "",
company: "",
role: "",
message: "",
},
mode: "onTouched",
});
const goNext = async () => {
const fields = stepFields[step];
const valid = await form.trigger(fields);
if (valid) setStep((s) => s + 1);
};
const goBack = () => setStep((s) => s - 1);
const onSubmit = (data: FormValues) => {
alert(JSON.stringify(data, null, 2));
};
return (
{stepSchemas.map((_, i) => (
))}
Step {step + 1} of {stepSchemas.length}
{step === 0 && (
)}
{step === 1 && (
)}
{step === 2 && (
)}
{step > 0 && (
Back
)}
{step < stepSchemas.length - 1 ? (
Continue
) : (
Submit
)}
);
}
```
## Styling
### Data Slots
Use `data-slot` attributes to target specific parts of the component:
| Slot name | Element |
| ------------------- | ----------------------- |
| `form-field` | Root `Field` wrapper |
| `field-label` | Label element |
| `field-description` | Description paragraph |
| `field-error` | Error message container |
### Customization Examples
```css
/* Increase spacing between form fields */
[data-slot="form-field"] {
@apply gap-2;
}
```
```tsx
{
/* Override width via className */
}
;
```
## API Reference
### FormField
A helper component that wires a `Controller` to `Field` + `FieldLabel` + `FieldDescription` + `FieldError`.
| Prop | Type | Default | Description |
| ------------- | -------------------- | ------- | ---------------------------------------- |
| `name` | `string` | - | Field name matching the form schema |
| `label` | `string` | - | Label text displayed above the input |
| `description` | `string` | - | Help text displayed below the label |
| `required` | `boolean` | - | Shows a required indicator on the label |
| `className` | `string` | - | Additional CSS classes on the root Field |
| `children` | `React.ReactElement` | - | The input component to wire up |
## Accessibility
### Keyboard Interactions
| Key | Action |
| ----- | ----------------------------------------- |
| `Tab` | Moves focus to the input within the field |
### ARIA Attributes
- The `Field` wrapper renders with `role="group"`.
- `FieldLabel` is associated with the input via Base UI's label mechanism.
- `FieldError` renders with `role="alert"` and `aria-live="polite"` for screen reader announcements.
- When `invalid` is true, `data-invalid` is set on the field for styling hooks.
# Input
> a styled text input field built on Base UI
URL: https://prototyper-ui.com/docs/components/input
Base UI reference: https://base-ui.com/react/components/input
```tsx
import { Input } from "@/components/ui/input";
export default function InputDemo() {
return ;
}
```
## Installation
```bash
pnpm dlx shadcn@latest add https://prototyper-ui.com/r/input.json
```
This will add the following files to your project:
- `components/ui/input.tsx`
## Usage
```tsx
import { Input } from "@/components/ui/input";
;
```
## Styling
### Data Slots
Use `data-slot` attributes to target the input in CSS:
| Slot name | Element |
| --------- | ------------------ |
| `input` | The ` ` root |
### Customization Examples
```css
/* Make all inputs taller */
[data-slot="input"] {
@apply h-12 text-lg;
}
```
```tsx
{
/* Override styles via className */
}
;
```
## API Reference
### Input
A styled text input built on Base UI `Input`.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `type` | `string` | - | HTML input type (e.g. "text", "email", "password") |
| `className` | `string` | - | Additional CSS classes |
All native `input` props and [Base UI Input props](https://base-ui.com/react/components/input) are forwarded via `...props`.
## Accessibility
### Keyboard Interactions
| Key | Action |
| ----- | ------------------------------------ |
| `Tab` | Moves focus into or out of the input |
### ARIA Attributes
- Renders as a native ` ` element via Base UI `Input`.
- `aria-invalid` styling is applied when the input has validation errors.
- Pair with a `` or `aria-label` to ensure the input is accessible to screen readers.
## Compose
This component is available in [`@prototyperco/compose`](/docs/compose).
### Catalog Definition
```typescript title="input.catalog.ts"
import { z } from "zod";
import { defineComponent } from "@prototyperco/compose/catalog";
export default defineComponent({
description: "A text input field for user data entry",
props: z.object({
label: z.string().optional().describe("Label displayed above the input"),
name: z.string().optional().describe("Field name for form submission"),
placeholder: z.string().optional().describe("Placeholder text"),
type: z
.enum(["text", "email", "password", "number", "search", "tel", "url"])
.optional()
.describe("Input type"),
disabled: z.boolean().optional().describe("Whether the input is disabled"),
readOnly: z.boolean().optional().describe("Whether the input is read-only"),
value: z.string().optional().describe("Controlled input value"),
}),
events: ["change", "focus", "blur", "submit"],
example: { label: "Name", placeholder: "Enter your name...", type: "text" },
});
```
### Example Spec
```json
{
"root": "nameInput",
"elements": {
"nameInput": {
"type": "Input",
"props": {
"label": "Name",
"placeholder": "Enter your name...",
"type": "text"
},
"$bindState": { "path": "/name", "event": "change" }
}
}
}
```
Learn more in the [Compose documentation](/docs/compose).
# InputGroup
> a composable input group with addons, buttons, and text
URL: https://prototyper-ui.com/docs/components/input-group
```tsx
import { ArrowRight } from "lucide-react";
import {
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupInput,
} from "@/components/ui/input-group";
export default function InputGroupDemo() {
return (
Subscribe
);
}
```
## Installation
```bash
pnpm dlx shadcn@latest add https://prototyper-ui.com/r/input-group.json
```
This will add the following files to your project:
- `components/ui/input-group.tsx`
> **Note:** This component depends on [Button](/docs/components/button), [Input](/docs/components/input), and Textarea. They will be installed automatically.
## Usage
```tsx
import {
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupInput,
} from "@/components/ui/input-group";
Go
;
```
## Anatomy
```tsx
```
Or with a textarea:
```tsx
```
| Sub-component | `data-slot` | Purpose | Required |
| -------------------- | --------------------- | ------------------------------------------------ | -------- |
| `InputGroup` | `input-group` | Root wrapper, provides group styling context | Yes |
| `InputGroupAddon` | `input-group-addon` | Container for icons, text, buttons, or kbd hints | No |
| `InputGroupButton` | - | A compact button styled for use inside the group | No |
| `InputGroupText` | - | Inline text or icon container | No |
| `InputGroupInput` | `input-group-control` | Single-line text input (borderless) | Yes\* |
| `InputGroupTextarea` | `input-group-control` | Multi-line textarea (borderless) | Yes\* |
\* Use either `InputGroupInput` or `InputGroupTextarea`, not both.
## Styling
### Data Slots
Use `data-slot` attributes to target specific parts of the input group:
| Slot name | Element |
| --------------------- | -------------------------------- |
| `input-group` | Root wrapper `` |
| `input-group-addon` | Addon container (icons, buttons) |
| `input-group-control` | The input or textarea element |
### Customization Examples
```css
/* Remove border from all input groups */
[data-slot="input-group"] {
@apply border-0 shadow-none;
}
/* Style addons */
[data-slot="input-group-addon"] {
@apply text-foreground;
}
```
```tsx
{
/* Override styles via className */
}
;
```
## API Reference
### InputGroup
Root wrapper that provides grouped styling and focus management.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `className` | `string` | - | Additional CSS classes |
| `children` | `React.ReactNode` | - | Group content (addons, input, buttons) |
All native `div` props are forwarded via `...props`.
### InputGroupAddon
Container for icons, text, buttons, or keyboard shortcut hints.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `align` | `"inline-start" \| "inline-end" \| "block-start" \| "block-end"` | `"inline-start"` | Position of the addon relative to the input |
| `className` | `string` | - | Additional CSS classes |
| `children` | `React.ReactNode` | - | Addon content (icons, text, buttons) |
All native `div` props are forwarded via `...props`.
### InputGroupButton
A compact button styled for use inside the input group. Extends [Button](/docs/components/button) props.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `size` | `"xs" \| "sm" \| "icon-xs" \| "icon-sm"` | `"xs"` | Size of the button |
| `variant` | `ButtonProps["variant"]` | `"ghost"` | Visual style variant (from Button) |
| `type` | `"button" \| "submit" \| "reset"` | `"button"` | HTML button type |
| `className` | `string` | - | Additional CSS classes |
All [Button](/docs/components/button) props (except `size` and `type`) are forwarded via `...props`.
### InputGroupText
Inline text or icon container for display content inside the group.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `className` | `string` | - | Additional CSS classes |
| `children` | `React.ReactNode` | - | Text or icon content |
All native `span` props are forwarded via `...props`.
### InputGroupInput
A borderless single-line input styled for use inside the group. Built on [Input](/docs/components/input).
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `className` | `string` | - | Additional CSS classes |
All native `input` props are forwarded via `...props`.
### InputGroupTextarea
A borderless multi-line textarea styled for use inside the group. Built on Textarea.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `className` | `string` | - | Additional CSS classes |
All native `textarea` props are forwarded via `...props`.
## Accessibility
### Keyboard Interactions
| Key | Action |
| ----- | --------------------------------------------- |
| `Tab` | Moves focus into or out of the input/textarea |
### ARIA Attributes
- `InputGroup` renders with `role="group"` to associate the input with its addons.
- `InputGroupAddon` renders with `role="group"` for semantic grouping.
- Clicking an addon area focuses the input for better usability.
- Pair with a `
` or `aria-label` on the input to ensure accessibility.
# Label
> a form label with disabled and invalid state styling
URL: https://prototyper-ui.com/docs/components/label
Label renders a styled native `` element for form controls. It picks up disabled and invalid state from a surrounding `Field` automatically, so you rarely have to wire styling by hand — the label dims when the input is disabled and turns red when the field is marked invalid. Use it for every form input you ship, both for accessibility and because users rely on visible labels far more than placeholder text alone.
```tsx
import { Label } from "@/components/ui/label";
export default function LabelDemo() {
return Email address ;
}
```
### When to use
- Any time a form control needs a visible, persistent name — inputs, selects, switches, checkboxes, radio groups.
- When you want disabled and invalid styling to flow from a parent `Field` without writing conditional class names.
- When you need a clickable target that focuses or toggles the associated control, since the native `` element handles that for free.
Avoid using `Label` purely as decorative text — for headings, callouts, or section titles, reach for a semantic heading element instead so screen readers do not announce it as a form label.
## Installation
```bash
pnpm dlx shadcn@latest add https://prototyper-ui.com/r/label.json
```
This will add the following files to your project:
- `components/ui/label.tsx`
## Usage
```tsx
import { Label } from "@/components/ui/label";
```
```tsx
Email address
```
## Examples
### With Input
```tsx
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
export default function LabelWithInput() {
return (
Email
);
}
```
## Styling
### Data Slots
Use `data-slot` attributes to target the label in CSS:
| Slot name | Element |
| --------- | ------------------ |
| `label` | The `` root |
### Customization Examples
```css
/* Make all labels uppercase */
[data-slot="label"] {
@apply text-xs uppercase tracking-wide;
}
```
```tsx
Custom label
```
## API Reference
### Label
A styled `` element for form controls.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `className` | `string` | - | Additional CSS classes. |
| `children` | `React.ReactNode` | - | Label content. |
All standard `label` element props are forwarded via `...props`.
## Accessibility
### Keyboard Interactions
Label is a non-interactive display element and does not have keyboard interactions. Clicking a label focuses or activates its associated form control via the `htmlFor` attribute or nesting.
### ARIA Attributes
- Renders as a native `` element, so assistive technology announces it as the accessible name of the associated control automatically.
- Associate with a form control either by setting `htmlFor` to the control's `id`, or by nesting the control inside the label. Either pattern works; pick the one that fits your layout.
- Automatically receives `data-disabled` and `data-invalid` styling from parent `Field` or group contexts, so you do not need to mirror state on the label itself.
- If you need a hidden label for an input that has a visible icon or placeholder, prefer `aria-label` on the input over a visually-hidden Label — it keeps the markup simpler and avoids stale labels.
# Menu
> dropdown menu with submenus and selection built on base ui
URL: https://prototyper-ui.com/docs/components/menu
Base UI reference: https://base-ui.com/react/components/menu
```tsx
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/menu";
import { Button } from "@/components/ui/button";
export default function MenuDemo() {
return (
}
>
☰
alert("open")}>Open
alert("rename")}>
Rename…
alert("duplicate")}>
Duplicate
alert("share")}>
Share…
alert("delete")}>
Delete…
);
}
```
## Installation
```bash
pnpm dlx shadcn@latest add https://prototyper-ui.com/r/menu.json
```
This will add the following files to your project:
- `components/ui/menu.tsx`
## Usage
```tsx
import {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
} from "@/components/ui/menu";
Open Menu
Item 1
Item 2
;
```
## Anatomy
```tsx
```
| Sub-component | `data-slot` | Purpose | Required |
| -------------------------- | ----------------------------- | --------------------------------------- | -------- |
| `DropdownMenu` | `dropdown-menu` | Root provider, manages open/close state | Yes |
| `DropdownMenuTrigger` | `dropdown-menu-trigger` | Button that opens the menu | Yes |
| `DropdownMenuPortal` | `dropdown-menu-portal` | Renders children into a portal | No |
| `DropdownMenuContent` | `dropdown-menu-content` | The popup panel containing menu items | Yes |
| `DropdownMenuGroup` | `dropdown-menu-group` | Groups related menu items | No |
| `DropdownMenuLabel` | `dropdown-menu-label` | Non-interactive label for a group | No |
| `DropdownMenuItem` | `dropdown-menu-item` | Individual actionable menu item | Yes |
| `DropdownMenuCheckboxItem` | `dropdown-menu-checkbox-item` | Toggleable checkbox menu item | No |
| `DropdownMenuRadioGroup` | `dropdown-menu-radio-group` | Groups radio menu items | No |
| `DropdownMenuRadioItem` | `dropdown-menu-radio-item` | Radio-selectable menu item | No |
| `DropdownMenuSeparator` | `dropdown-menu-separator` | Visual separator between items | No |
| `DropdownMenuShortcut` | `dropdown-menu-shortcut` | Keyboard shortcut hint text | No |
| `DropdownMenuSub` | `dropdown-menu-sub` | Root for a submenu | No |
| `DropdownMenuSubTrigger` | `dropdown-menu-sub-trigger` | Item that opens a submenu | No |
| `DropdownMenuSubContent` | `dropdown-menu-sub-content` | Popup panel for a submenu | No |
## Examples
### Content
```tsx
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/menu";
import { Button } from "@/components/ui/button";
export default function MenuContent() {
const items = [
{ id: 1, name: "New" },
{ id: 2, name: "Open" },
{ id: 3, name: "Close" },
{ id: 4, name: "Save" },
{ id: 5, name: "Duplicate" },
{ id: 6, name: "Rename" },
{ id: 7, name: "Move" },
];
return (
}>
Actions
{items.map((item) => (
{item.name}
))}
);
}
```
### Disabled Items
```tsx
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/menu";
import { Button } from "@/components/ui/button";
export default function MenuDisabledItems() {
return (
}>
Actions
Copy
Cut
Paste
);
}
```
### Disabled Keys
```tsx
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/menu";
import { Button } from "@/components/ui/button";
export default function MenuDisabledKeys() {
const items = [
{ id: 1, name: "New" },
{ id: 2, name: "Open" },
{ id: 3, name: "Close" },
{ id: 4, name: "Save" },
{ id: 5, name: "Duplicate" },
{ id: 6, name: "Rename" },
{ id: 7, name: "Move" },
];
const disabledKeys = [4, 6];
return (
}>
Actions
{items.map((item) => (
{item.name}
))}
);
}
```
### Links
```tsx
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/menu";
import { Button } from "@/components/ui/button";
export default function MenuLinks() {
return (
}>
Links
Adobe
Apple
Google
Microsoft
);
}
```
### Long Press
```tsx
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/menu";
import { Button } from "@/components/ui/button";
export default function MenuLongPress() {
return (
alert("crop")}
onContextMenu={(e: React.MouseEvent) => e.preventDefault()}
/>
}
>
Crop
Rotate
Slice
Clone stamp
);
}
```
### Reusable
```tsx
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/menu";
import { Button } from "@/components/ui/button";
function ProtoMenu({
label,
variant = "outline",
children,
}: {
label: string;
variant?:
| "outline"
| "default"
| "ghost"
| "secondary"
| "destructive"
| "link";
children: React.ReactNode;
}) {
return (
}>
{label}
{children}
);
}
export default function MenuReusable() {
return (
Cut
Copy
Paste
Delete Item
);
}
```
### Sections
```tsx
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuTrigger,
} from "@/components/ui/menu";
import { Button } from "@/components/ui/button";
export default function MenuSections() {
return (
}>
Actions
Styles
Bold
Underline
Align
Left
Middle
Right
);
}
```
### Sections Dynamic
```tsx
"use client";
import React from "react";
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuLabel,
DropdownMenuTrigger,
} from "@/components/ui/menu";
import { Button } from "@/components/ui/button";
export default function MenuSectionsDynamic() {
const [selected, setSelected] = React.useState>({
1: true,
3: true,
});
const openWindows = [
{
name: "Left Panel",
id: "left",
children: [{ id: 1, name: "Final Copy (1)" }],
},
{
name: "Right Panel",
id: "right",
children: [
{ id: 2, name: "index.ts" },
{ id: 3, name: "package.json" },
{ id: 4, name: "license.txt" },
],
},
];
const handleCheckedChange = (id: number, checked: boolean) => {
setSelected((prev) => ({ ...prev, [id]: checked }));
};
return (
}>
Window
{openWindows.map((section) => (
{section.name}
{section.children.map((item) => (
handleCheckedChange(item.id, checked)
}
>
{item.name}
))}
))}
);
}
```
### Selection Multiple
```tsx
"use client";
import React from "react";
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuTrigger,
} from "@/components/ui/menu";
import { Button } from "@/components/ui/button";
export default function MenuSelectionMultiple() {
const [sidebar, setSidebar] = React.useState(true);
const [searchbar, setSearchbar] = React.useState(false);
const [tools, setTools] = React.useState(false);
const [console, setConsole] = React.useState(true);
return (
}>
View
Sidebar
Searchbar
Tools
Console
);
}
```
### Selection Single
```tsx
"use client";
import React from "react";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuTrigger,
} from "@/components/ui/menu";
import { Button } from "@/components/ui/button";
export default function MenuSelectionSingle() {
const [selected, setSelected] = React.useState("center");
return (
}>
Align
Left
Center
Right
);
}
```
### Separators
```tsx
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/menu";
import { Button } from "@/components/ui/button";
export default function MenuSeparators() {
return (
}>
Actions
New…
Open…
Save
Save as…
Rename…
Page setup…
Print…
);
}
```
### Sub Menu
```tsx
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuTrigger,
} from "@/components/ui/menu";
import { Button } from "@/components/ui/button";
export default function MenuSubMenus() {
return (
}>
Actions
Copy
Cut
Delete
Share
SMS
Twitter
Email
Work
Personal
);
}
```
### Sub Menu Dynamic
```tsx
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuTrigger,
} from "@/components/ui/menu";
import { Button } from "@/components/ui/button";
type MenuItem = {
id: string;
name: string;
children?: MenuItem[];
};
export default function MenuSubMenuDynamic() {
const items: MenuItem[] = [
{ id: "cut", name: "Cut" },
{ id: "copy", name: "Copy" },
{ id: "delete", name: "Delete" },
{
id: "share",
name: "Share",
children: [
{ id: "sms", name: "SMS" },
{ id: "twitter", name: "Twitter" },
{
id: "email",
name: "Email",
children: [
{ id: "work", name: "Work" },
{ id: "personal", name: "Personal" },
],
},
],
},
];
function renderSubmenu(item: MenuItem) {
if (item.children) {
return (
{item.name}
{item.children.map((child) => renderSubmenu(child))}
);
} else {
return {item.name} ;
}
}
return (
}>
Actions
{items.map((item) => renderSubmenu(item))}
);
}
```
### Text Slots
```tsx
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuShortcut,
DropdownMenuTrigger,
} from "@/components/ui/menu";
import { Button } from "@/components/ui/button";
export default function MenuTextSlots() {
return (
}>
Actions
Copy
Copy the selected text
⌘C
Cut
Cut the selected text
⌘X
Paste
Paste the copied text
⌘V
);
}
```
## Styling
### Data Slots
Use `data-slot` attributes to target specific parts of the menu:
| Slot name | Element |
| ----------------------------- | ------------------------------- |
| `dropdown-menu` | Root provider (no DOM rendered) |
| `dropdown-menu-trigger` | The trigger button |
| `dropdown-menu-portal` | Portal wrapper |
| `dropdown-menu-content` | The popup panel |
| `dropdown-menu-group` | Group wrapper |
| `dropdown-menu-label` | Group label |
| `dropdown-menu-item` | Individual menu item |
| `dropdown-menu-checkbox-item` | Checkbox menu item |
| `dropdown-menu-radio-group` | Radio group wrapper |
| `dropdown-menu-radio-item` | Radio menu item |
| `dropdown-menu-separator` | Visual separator line |
| `dropdown-menu-shortcut` | Keyboard shortcut text |
| `dropdown-menu-sub` | Submenu root |
| `dropdown-menu-sub-trigger` | Submenu trigger item |
| `dropdown-menu-sub-content` | Submenu popup panel |
### Customization Examples
```css
/* Change menu item highlight color */
[data-slot="dropdown-menu-item"][data-highlighted] {
@apply bg-primary text-primary-foreground;
}
/* Wider menu popup */
[data-slot="dropdown-menu-content"] {
@apply min-w-[12rem];
}
```
```tsx
{
/* Override styles via className */
}
Custom Item
;
```
## API Reference
### DropdownMenu
Root component that manages open/close state.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `open` | `boolean` | - | Controlled open state |
| `onOpenChange` | `(open: boolean) => void` | - | Callback when open state changes |
All [Base UI Menu.Root props](https://base-ui.com/react/components/menu) are forwarded via `...props`.
### DropdownMenuTrigger
Button that opens the menu when clicked.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `className` | `string` | - | Additional CSS classes |
All [Base UI Menu.Trigger props](https://base-ui.com/react/components/menu) are forwarded via `...props`.
### DropdownMenuContent
The popup panel containing menu items, rendered inside a portal with a positioner.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `align` | `"start" \| "center" \| "end"` | `"start"` | Alignment relative to the trigger |
| `alignOffset` | `number` | `0` | Offset from the alignment edge |
| `side` | `"top" \| "bottom" \| "left" \| "right"` | `"bottom"` | Preferred side relative to trigger |
| `sideOffset` | `number` | `4` | Gap between trigger and popup |
| `className` | `string` | - | Additional CSS classes |
| `children` | `React.ReactNode` | - | Menu content |
All [Base UI Menu.Popup props](https://base-ui.com/react/components/menu) are forwarded via `...props`.
### DropdownMenuGroup
Groups related menu items together.
All [Base UI Menu.Group props](https://base-ui.com/react/components/menu) are forwarded via `...props`.
### DropdownMenuLabel
Non-interactive label for a group of menu items.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `inset` | `boolean` | - | Adds left padding to align with items that have icons |
| `className` | `string` | - | Additional CSS classes |
All [Base UI Menu.GroupLabel props](https://base-ui.com/react/components/menu) are forwarded via `...props`.
### DropdownMenuItem
Individual actionable menu item.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `variant` | `"default" \| "destructive"` | `"default"` | Visual style variant |
| `inset` | `boolean` | - | Adds left padding to align with items that have icons |
| `className` | `string` | - | Additional CSS classes |
All [Base UI Menu.Item props](https://base-ui.com/react/components/menu) are forwarded via `...props`.
### DropdownMenuCheckboxItem
Toggleable checkbox menu item with a check indicator.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `checked` | `boolean` | - | Whether the item is checked |
| `inset` | `boolean` | - | Adds left padding to align with items that have icons |
| `className` | `string` | - | Additional CSS classes |
| `children` | `React.ReactNode` | - | Item content |
All [Base UI Menu.CheckboxItem props](https://base-ui.com/react/components/menu) are forwarded via `...props`.
### DropdownMenuRadioGroup
Groups radio menu items for single-selection behavior.
All [Base UI Menu.RadioGroup props](https://base-ui.com/react/components/menu) are forwarded via `...props`.
### DropdownMenuRadioItem
Radio-selectable menu item within a radio group.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `inset` | `boolean` | - | Adds left padding to align with items that have icons |
| `className` | `string` | - | Additional CSS classes |
| `children` | `React.ReactNode` | - | Item content |
All [Base UI Menu.RadioItem props](https://base-ui.com/react/components/menu) are forwarded via `...props`.
### DropdownMenuSeparator
Visual separator between menu items or groups.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `className` | `string` | - | Additional CSS classes |
All [Base UI Menu.Separator props](https://base-ui.com/react/components/menu) are forwarded via `...props`.
### DropdownMenuShortcut
Displays a keyboard shortcut hint aligned to the right of a menu item.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `className` | `string` | - | Additional CSS classes |
All standard `span` props are forwarded via `...props`.
### DropdownMenuSub
Root provider for a submenu.
All [Base UI Menu.SubmenuRoot props](https://base-ui.com/react/components/menu) are forwarded via `...props`.
### DropdownMenuSubTrigger
Menu item that opens a submenu on hover or keyboard navigation.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `inset` | `boolean` | - | Adds left padding to align with items that have icons |
| `className` | `string` | - | Additional CSS classes |
| `children` | `React.ReactNode` | - | Trigger content |
All [Base UI Menu.SubmenuTrigger props](https://base-ui.com/react/components/menu) are forwarded via `...props`.
### DropdownMenuSubContent
Popup panel for a submenu, rendered inside a portal with a positioner.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `align` | `"start" \| "center" \| "end"` | `"start"` | Alignment relative to the trigger |
| `alignOffset` | `number` | `-3` | Offset from the alignment edge |
| `side` | `"top" \| "bottom" \| "left" \| "right"` | `"right"` | Preferred side relative to trigger |
| `sideOffset` | `number` | `0` | Gap between trigger and popup |
| `className` | `string` | - | Additional CSS classes |
All `DropdownMenuContent` props are forwarded via `...props`.
## Accessibility
### Keyboard Interactions
| Key | Action |
| ------------ | -------------------------------------------------------- |
| `Space` | Activates the focused menu item |
| `Enter` | Activates the focused menu item |
| `ArrowDown` | Moves focus to the next menu item |
| `ArrowUp` | Moves focus to the previous menu item |
| `ArrowRight` | Opens a submenu when focused on a sub-trigger |
| `ArrowLeft` | Closes a submenu and returns focus to the parent trigger |
| `Escape` | Closes the menu |
| `Home` | Moves focus to the first menu item |
| `End` | Moves focus to the last menu item |
### ARIA Attributes
- `DropdownMenuContent` receives `role="menu"`.
- `DropdownMenuItem` receives `role="menuitem"`.
- `DropdownMenuCheckboxItem` receives `role="menuitemcheckbox"` with `aria-checked`.
- `DropdownMenuRadioItem` receives `role="menuitemradio"` with `aria-checked`.
- `DropdownMenuSub` content receives `role="menu"` for nested menus.
- `data-disabled` is set on disabled items, which sets `aria-disabled`.
- Focus is managed within the menu and returns to the trigger when the menu closes.
# Menubar
> A horizontal menu bar with multiple dropdown menus, built on Base UI
URL: https://prototyper-ui.com/docs/components/menubar
Base UI reference: https://base-ui.com/react/components/menubar
```tsx
"use client";
import {
Menubar,
MenubarContent,
MenubarItem,
MenubarMenu,
MenubarSeparator,
MenubarShortcut,
MenubarSub,
MenubarSubContent,
MenubarSubTrigger,
MenubarTrigger,
} from "@/components/ui/menubar";
export default function MenubarDemo() {
return (
File
New Tab Ctrl+T
New Window Ctrl+N
Share
Email
Messages
Print Ctrl+P
Edit
Undo Ctrl+Z
Redo Ctrl+Y
Cut Ctrl+X
Copy Ctrl+C
Paste Ctrl+V
View
Zoom In Ctrl++
Zoom Out Ctrl+-
Full Screen F11
);
}
```
## Installation
```bash
pnpm dlx shadcn@latest add https://prototyper-ui.com/r/menubar.json
```
This will add the following files to your project:
- `components/ui/menubar.tsx`
- `components/ui/menu.tsx` (dependency)
## Usage
```tsx
import {
Menubar,
MenubarMenu,
MenubarTrigger,
MenubarContent,
MenubarItem,
} from "@/components/ui/menubar";
File
New File
Open
Save
;
```
## Anatomy
```tsx
```
| Sub-component | `data-slot` | Purpose | Required |
| --------------------- | ----------------------- | ------------------------------------- | -------- |
| `Menubar` | `menubar` | Root container, horizontal bar | Yes |
| `MenubarMenu` | `menubar-menu` | Individual menu within the bar | Yes |
| `MenubarTrigger` | `menubar-trigger` | Button that opens a menu | Yes |
| `MenubarPortal` | `menubar-portal` | Renders children into a portal | No |
| `MenubarContent` | `menubar-content` | The popup panel containing menu items | Yes |
| `MenubarGroup` | `menubar-group` | Groups related menu items | No |
| `MenubarLabel` | `menubar-label` | Non-interactive label for a group | No |
| `MenubarItem` | `menubar-item` | Individual actionable menu item | Yes |
| `MenubarCheckboxItem` | `menubar-checkbox-item` | Toggleable checkbox menu item | No |
| `MenubarRadioGroup` | `menubar-radio-group` | Groups radio menu items | No |
| `MenubarRadioItem` | `menubar-radio-item` | Radio-selectable menu item | No |
| `MenubarSeparator` | `menubar-separator` | Visual separator between items | No |
| `MenubarShortcut` | `menubar-shortcut` | Keyboard shortcut hint text | No |
| `MenubarSub` | `menubar-sub` | Root for a submenu | No |
| `MenubarSubTrigger` | `menubar-sub-trigger` | Item that opens a submenu | No |
| `MenubarSubContent` | `menubar-sub-content` | Popup panel for a submenu | No |
## Examples
### Application Menu
```tsx
"use client";
import {
Menubar,
MenubarContent,
MenubarItem,
MenubarMenu,
MenubarSeparator,
MenubarShortcut,
MenubarSub,
MenubarSubContent,
MenubarSubTrigger,
MenubarTrigger,
} from "@/components/ui/menubar";
export default function MenubarDemo() {
return (
File
New Tab Ctrl+T
New Window Ctrl+N
Share
Email
Messages
Print Ctrl+P
Edit
Undo Ctrl+Z
Redo Ctrl+Y
Cut Ctrl+X
Copy Ctrl+C
Paste Ctrl+V
View
Zoom In Ctrl++
Zoom Out Ctrl+-
Full Screen F11
);
}
```
## Styling
### Data Slots
Use `data-slot` attributes to target specific parts of the menubar:
| Slot name | Element |
| ----------------------- | ---------------------------- |
| `menubar` | The horizontal bar container |
| `menubar-menu` | Individual menu root |
| `menubar-trigger` | The trigger button |
| `menubar-portal` | Portal wrapper |
| `menubar-content` | The popup panel |
| `menubar-group` | Group wrapper |
| `menubar-label` | Group label |
| `menubar-item` | Individual menu item |
| `menubar-checkbox-item` | Checkbox menu item |
| `menubar-radio-group` | Radio group wrapper |
| `menubar-radio-item` | Radio menu item |
| `menubar-separator` | Visual separator line |
| `menubar-shortcut` | Keyboard shortcut text |
| `menubar-sub` | Submenu root |
| `menubar-sub-trigger` | Submenu trigger item |
| `menubar-sub-content` | Submenu popup panel |
### Customization Examples
```css
/* Change menubar background */
[data-slot="menubar"] {
@apply bg-muted;
}
/* Change menu item highlight color */
[data-slot="menubar-item"][data-highlighted] {
@apply bg-primary text-primary-foreground;
}
```
```tsx
{
/* Override styles via className */
}
File
Custom Item
;
```
## API Reference
### Menubar
Root container that renders a horizontal menu bar.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `className` | `string` | - | Additional CSS classes |
All [Base UI Menubar props](https://base-ui.com/react/components/menubar) are forwarded via `...props`.
### MenubarMenu
Individual menu within the menu bar. Wraps `DropdownMenu`.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `open` | `boolean` | - | Controlled open state |
| `onOpenChange` | `(open: boolean) => void` | - | Callback when open state changes |
All [Base UI Menu.Root props](https://base-ui.com/react/components/menu) are forwarded via `...props`.
### MenubarTrigger
Button within the menu bar that opens a dropdown menu.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `className` | `string` | - | Additional CSS classes |
All [Base UI Menu.Trigger props](https://base-ui.com/react/components/menu) are forwarded via `...props`.
### MenubarContent
The popup panel containing menu items, rendered inside a portal with a positioner.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `align` | `"start" \| "center" \| "end"` | `"start"` | Alignment relative to the trigger |
| `alignOffset` | `number` | `-4` | Offset from the alignment edge |
| `sideOffset` | `number` | `8` | Gap between trigger and popup |
| `className` | `string` | - | Additional CSS classes |
| `children` | `React.ReactNode` | - | Menu content |
All [Base UI Menu.Popup props](https://base-ui.com/react/components/menu) are forwarded via `...props`.
### MenubarGroup
Groups related menu items together.
All [Base UI Menu.Group props](https://base-ui.com/react/components/menu) are forwarded via `...props`.
### MenubarLabel
Non-interactive label for a group of menu items.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `inset` | `boolean` | - | Adds left padding to align with items that have icons |
| `className` | `string` | - | Additional CSS classes |
All [Base UI Menu.GroupLabel props](https://base-ui.com/react/components/menu) are forwarded via `...props`.
### MenubarItem
Individual actionable menu item.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `variant` | `"default" \| "destructive"` | `"default"` | Visual style variant |
| `inset` | `boolean` | - | Adds left padding to align with items that have icons |
| `className` | `string` | - | Additional CSS classes |
All [Base UI Menu.Item props](https://base-ui.com/react/components/menu) are forwarded via `...props`.
### MenubarCheckboxItem
Toggleable checkbox menu item with a check indicator.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `checked` | `boolean` | - | Whether the item is checked |
| `inset` | `boolean` | - | Adds left padding to align with items that have icons |
| `className` | `string` | - | Additional CSS classes |
| `children` | `React.ReactNode` | - | Item content |
All [Base UI Menu.CheckboxItem props](https://base-ui.com/react/components/menu) are forwarded via `...props`.
### MenubarRadioGroup
Groups radio menu items for single-selection behavior.
All [Base UI Menu.RadioGroup props](https://base-ui.com/react/components/menu) are forwarded via `...props`.
### MenubarRadioItem
Radio-selectable menu item within a radio group.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `inset` | `boolean` | - | Adds left padding to align with items that have icons |
| `className` | `string` | - | Additional CSS classes |
| `children` | `React.ReactNode` | - | Item content |
All [Base UI Menu.RadioItem props](https://base-ui.com/react/components/menu) are forwarded via `...props`.
### MenubarSeparator
Visual separator between menu items or groups.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `className` | `string` | - | Additional CSS classes |
All [Base UI Menu.Separator props](https://base-ui.com/react/components/menu) are forwarded via `...props`.
### MenubarShortcut
Displays a keyboard shortcut hint aligned to the right of a menu item.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `className` | `string` | - | Additional CSS classes |
All standard `span` props are forwarded via `...props`.
### MenubarSub
Root provider for a submenu.
All [Base UI Menu.SubmenuRoot props](https://base-ui.com/react/components/menu) are forwarded via `...props`.
### MenubarSubTrigger
Menu item that opens a submenu on hover or keyboard navigation.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `inset` | `boolean` | - | Adds left padding to align with items that have icons |
| `className` | `string` | - | Additional CSS classes |
| `children` | `React.ReactNode` | - | Trigger content |
All [Base UI Menu.SubmenuTrigger props](https://base-ui.com/react/components/menu) are forwarded via `...props`.
### MenubarSubContent
Popup panel for a submenu, rendered inside a portal with a positioner.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `align` | `"start" \| "center" \| "end"` | `"start"` | Alignment relative to the trigger |
| `alignOffset` | `number` | `-3` | Offset from the alignment edge |
| `side` | `"top" \| "bottom" \| "left" \| "right"` | `"right"` | Preferred side relative to trigger |
| `sideOffset` | `number` | `0` | Gap between trigger and popup |
| `className` | `string` | - | Additional CSS classes |
All [Base UI Menu.SubmenuPopup props](https://base-ui.com/react/components/menu) are forwarded via `...props`.
## Accessibility
### Keyboard Interactions
| Key | Action |
| ------------ | ------------------------------------------- |
| `Space` | Activates the focused menu item |
| `Enter` | Activates the focused menu item |
| `ArrowDown` | Moves focus to the next menu item |
| `ArrowUp` | Moves focus to the previous menu item |
| `ArrowRight` | Opens the next menu or a submenu |
| `ArrowLeft` | Opens the previous menu or closes a submenu |
| `Escape` | Closes the currently open menu |
| `Home` | Moves focus to the first menu item |
| `End` | Moves focus to the last menu item |
### ARIA Attributes
- `Menubar` receives `role="menubar"`.
- `MenubarTrigger` receives `role="menuitem"` within the menubar context.
- `MenubarContent` receives `role="menu"`.
- `MenubarItem` receives `role="menuitem"`.
- `MenubarCheckboxItem` receives `role="menuitemcheckbox"` with `aria-checked`.
- `MenubarRadioItem` receives `role="menuitemradio"` with `aria-checked`.
- `MenubarSub` content receives `role="menu"` for nested menus.
- `data-disabled` is set on disabled items, which sets `aria-disabled`.
- Focus is managed within the menu bar and returns to the trigger when a menu closes.
# Meter
> A meter displaying a value within a known range
URL: https://prototyper-ui.com/docs/components/meter
Base UI reference: https://base-ui.com/react/components/meter
```tsx
"use client";
import React from "react";
import {
Meter,
MeterIndicator,
MeterLabel,
MeterTrack,
MeterValue,
} from "@/components/ui/meter";
export default function MeterDemo() {
const [progress, setProgress] = React.useState(13);
React.useEffect(() => {
const timer = setTimeout(() => setProgress(66), 500);
return () => clearTimeout(timer);
}, []);
return (
Storage space
);
}
```
## Installation
```bash
pnpm dlx shadcn@latest add https://prototyper-ui.com/r/meter.json
```
This will add the following files to your project:
- `components/ui/meter.tsx`
## Usage
```tsx
import {
Meter,
MeterTrack,
MeterIndicator,
MeterLabel,
MeterValue,
} from "@/components/ui/meter";
Storage
;
```
## Anatomy
```tsx
```
| Sub-component | `data-slot` | Purpose | Required |
| ---------------- | ----------------- | ------------------------------------- | -------- |
| `Meter` | `meter` | Root provider, manages value state | Yes |
| `MeterTrack` | `meter-track` | Background track for the indicator | Yes |
| `MeterIndicator` | `meter-indicator` | Filled portion representing the value | Yes |
| `MeterLabel` | `meter-label` | Accessible label for the meter | No |
| `MeterValue` | `meter-value` | Displays the current value as text | No |
## Examples
### Custom Format
```tsx
import {
Meter,
MeterIndicator,
MeterLabel,
MeterTrack,
} from "@/components/ui/meter";
export default function MeterCustomFormat() {
return (
Space used
54 of 60GB
);
}
```
### Reusable
```tsx
import {
Meter,
MeterIndicator,
MeterLabel,
MeterTrack,
MeterValue,
} from "@/components/ui/meter";
export default function MeterReusable() {
return (
Storage space
);
}
```
### Value Format
```tsx
import {
Meter,
MeterIndicator,
MeterLabel,
MeterTrack,
MeterValue,
} from "@/components/ui/meter";
export default function MeterValueFormat() {
return (
Currency
);
}
```
## Styling
### Data Slots
Use `data-slot` attributes to target specific parts of the meter:
| Slot name | Element |
| ----------------- | -------------------- |
| `meter` | Root wrapper |
| `meter-track` | Background track bar |
| `meter-indicator` | Filled indicator bar |
| `meter-label` | Label text |
| `meter-value` | Value text |
### Customization Examples
```css
/* Make the meter track taller */
[data-slot="meter-track"] {
@apply h-4 rounded-lg;
}
/* Custom indicator color */
[data-slot="meter-indicator"] {
@apply bg-gradient-to-r from-blue-500 to-purple-500;
}
```
```tsx
{
/* Override track color via className */
}
;
```
## API Reference
### Meter
Root component that provides meter value context.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `value` | `number` | - | Current value of the meter |
| `min` | `number` | `0` | Minimum value |
| `max` | `number` | `100` | Maximum value |
| `className` | `string` | - | Additional CSS classes |
| `children` | `React.ReactNode` | - | Meter content |
All [Base UI Meter.Root props](https://base-ui.com/react/components/meter) are forwarded via `...props`.
### MeterTrack
Background track that contains the indicator.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `color` | `"default" \| "success" \| "warning" \| "destructive" \| "info"` | `"default"` | Color variant for the track |
| `size` | `"sm" \| "md" \| "lg" \| "xl"` | `"md"` | Track height variant |
| `className` | `string` | - | Additional CSS classes |
| `children` | `React.ReactNode` | - | Track content (typically MeterIndicator) |
All [Base UI Meter.Track props](https://base-ui.com/react/components/meter) are forwarded via `...props`.
### MeterIndicator
The filled portion of the track representing the current value.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `color` | `"default" \| "success" \| "warning" \| "destructive"` | `"default"` | Color variant for the indicator |
| `className` | `string` | - | Additional CSS classes |
All [Base UI Meter.Indicator props](https://base-ui.com/react/components/meter) are forwarded via `...props`.
### MeterLabel
Accessible label for the meter.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `className` | `string` | - | Additional CSS classes |
| `children` | `React.ReactNode` | - | Label text |
All [Base UI Meter.Label props](https://base-ui.com/react/components/meter) are forwarded via `...props`.
### MeterValue
Displays the current meter value as formatted text.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `className` | `string` | - | Additional CSS classes |
| `children` | `React.ReactNode` | - | Custom value rendering |
All [Base UI Meter.Value props](https://base-ui.com/react/components/meter) are forwarded via `...props`.
### meterTrackVariants
A `cva` helper exported for applying meter track styles outside of the `` component.
```tsx
import { meterTrackVariants } from "@/components/ui/meter";
Custom track
;
```
### meterIndicatorVariants
A `cva` helper exported for applying meter indicator styles outside of the `` component.
```tsx
import { meterIndicatorVariants } from "@/components/ui/meter";
Custom indicator
;
```
## Accessibility
### Keyboard Interactions
The meter is a non-interactive display element and does not have keyboard interactions. Focus management is handled by surrounding interactive elements.
### ARIA Attributes
- The meter renders with `role="meter"` via the Base UI primitive.
- `aria-valuenow` reflects the current value.
- `aria-valuemin` and `aria-valuemax` define the range.
- `aria-labelledby` is automatically linked to `MeterLabel` when present.
- Screen readers announce the meter value and its label.
# Navigation Menu
> site navigation component with dropdown menus on Base UI
URL: https://prototyper-ui.com/docs/components/navigation-menu
Base UI reference: https://base-ui.com/react/components/navigation-menu
```tsx
"use client";
import {
NavigationMenu,
NavigationMenuContent,
NavigationMenuItem,
NavigationMenuLink,
NavigationMenuList,
NavigationMenuTrigger,
navigationMenuTriggerStyle,
} from "@/components/ui/navigation-menu";
export default function NavigationMenuDemo() {
return (
Getting Started
Introduction
Learn the basics and get up and running quickly.
Installation
Step-by-step guide to install and configure.
Typography
Styles for headings, paragraphs, and lists.
CLI
Add components using the command line.
Components
Button
Trigger actions and events.
Dialog
Modal and non-modal overlays.
Select
Pick a value from a dropdown list.
Tabs
Organize content into tabbed panels.
Documentation
);
}
```
## Installation
```bash
pnpm dlx shadcn@latest add https://prototyper-ui.com/r/navigation-menu.json
```
This will add the following files to your project:
- `components/ui/navigation-menu.tsx`
## Usage
```tsx
import {
NavigationMenu,
NavigationMenuContent,
NavigationMenuItem,
NavigationMenuLink,
NavigationMenuList,
NavigationMenuTrigger,
} from "@/components/ui/navigation-menu";
Item One
Documentation
About
;
```
## Anatomy
```tsx
```
| Sub-component | `data-slot` | Purpose | Required |
| -------------------------- | ---------------------------- | -------------------------------------------------------------- | -------- |
| `NavigationMenu` | `navigation-menu` | Root provider, manages open state and renders the positioner | Yes |
| `NavigationMenuList` | `navigation-menu-list` | Container for menu items | Yes |
| `NavigationMenuItem` | `navigation-menu-item` | Wraps a single trigger + content pair or a standalone link | Yes |
| `NavigationMenuTrigger` | `navigation-menu-trigger` | Button that opens a dropdown content area | No |
| `NavigationMenuContent` | `navigation-menu-content` | Dropdown panel associated with a trigger | No |
| `NavigationMenuLink` | `navigation-menu-link` | Link item inside the content or used standalone | No |
| `NavigationMenuPositioner` | `navigation-menu-positioner` | Positions the popup relative to the trigger (rendered by root) | Auto |
| `NavigationMenuIndicator` | `navigation-menu-indicator` | Optional visual indicator arrow | No |
## Examples
### With Dropdowns
```tsx
"use client";
import {
NavigationMenu,
NavigationMenuContent,
NavigationMenuItem,
NavigationMenuLink,
NavigationMenuList,
NavigationMenuTrigger,
navigationMenuTriggerStyle,
} from "@/components/ui/navigation-menu";
export default function NavigationMenuDemo() {
return (
Getting Started
Introduction
Learn the basics and get up and running quickly.
Installation
Step-by-step guide to install and configure.
Typography
Styles for headings, paragraphs, and lists.
CLI
Add components using the command line.
Components
Button
Trigger actions and events.
Dialog
Modal and non-modal overlays.
Select
Pick a value from a dropdown list.
Tabs
Organize content into tabbed panels.
Documentation
);
}
```
## Styling
### Data Slots
Use `data-slot` attributes to target specific parts of the navigation menu:
| Slot name | Element |
| --------------------------------- | ------------------------------------- |
| `navigation-menu` | Root wrapper |
| `navigation-menu-list` | Container for items |
| `navigation-menu-item` | Individual item wrapper |
| `navigation-menu-trigger` | Trigger button that opens content |
| `navigation-menu-content` | Dropdown content panel |
| `navigation-menu-link` | Link inside content or standalone |
| `navigation-menu-positioner` | Floating positioner element |
| `navigation-menu-popup` | Popup container within the positioner |
| `navigation-menu-viewport` | Viewport inside the popup |
| `navigation-menu-indicator` | Optional indicator arrow |
| `navigation-menu-indicator-arrow` | Arrow element inside the indicator |
### Customization Examples
```css
/* Wider dropdown content */
[data-slot="navigation-menu-content"] {
@apply p-4;
}
/* Custom trigger active state */
[data-slot="navigation-menu-trigger"][data-popup-open] {
@apply bg-primary text-primary-foreground;
}
```
```tsx
{
/* Style the navigation menu list with additional gap */
}
Item
...
;
```
## API Reference
### NavigationMenu
Root component that manages navigation state and renders the popup positioner.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `align` | `"start" \| "center" \| "end"` | `"start"` | Alignment of the popup relative to the trigger |
| `className` | `string` | - | Additional CSS classes |
| `children` | `React.ReactNode` | - | Navigation menu content |
All [Base UI NavigationMenu.Root props](https://base-ui.com/react/components/navigation-menu) are forwarded via `...props`.
### NavigationMenuList
Container for navigation menu items.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `className` | `string` | - | Additional CSS classes |
| `children` | `React.ReactNode` | - | Navigation menu items |
All [Base UI NavigationMenu.List props](https://base-ui.com/react/components/navigation-menu) are forwarded via `...props`.
### NavigationMenuItem
Wraps a trigger + content pair or a standalone link.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `className` | `string` | - | Additional CSS classes |
| `children` | `React.ReactNode` | - | Trigger and content, or a standalone link |
All [Base UI NavigationMenu.Item props](https://base-ui.com/react/components/navigation-menu) are forwarded via `...props`.
### NavigationMenuTrigger
Button that opens the associated dropdown content.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `className` | `string` | - | Additional CSS classes |
| `children` | `React.ReactNode` | - | Trigger label content |
All [Base UI NavigationMenu.Trigger props](https://base-ui.com/react/components/navigation-menu) are forwarded via `...props`.
### NavigationMenuContent
Dropdown panel shown when its associated trigger is activated.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `className` | `string` | - | Additional CSS classes |
| `children` | `React.ReactNode` | - | Content items, typically NavigationMenuLink elements |
All [Base UI NavigationMenu.Content props](https://base-ui.com/react/components/navigation-menu) are forwarded via `...props`.
### NavigationMenuLink
A link element inside the navigation menu. Can be used within content dropdowns or as a standalone item.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `href` | `string` | - | URL the link points to |
| `active` | `boolean` | `false` | Whether the link is the currently active page |
| `className` | `string` | - | Additional CSS classes |
| `children` | `React.ReactNode` | - | Link label content |
All [Base UI NavigationMenu.Link props](https://base-ui.com/react/components/navigation-menu) are forwarded via `...props`.
### NavigationMenuPositioner
Controls the position of the popup. Rendered automatically by `NavigationMenu`; you do not need to add it manually.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `side` | `"top" \| "bottom" \| "left" \| "right"` | `"bottom"` | Which side of the trigger to place the popup |
| `sideOffset` | `number` | `8` | Distance from the trigger in pixels |
| `align` | `"start" \| "center" \| "end"` | `"start"` | Alignment of the popup along the trigger edge |
| `alignOffset` | `number` | `0` | Offset along the alignment axis in pixels |
| `className` | `string` | - | Additional CSS classes |
### NavigationMenuIndicator
Optional visual indicator arrow displayed below the active trigger.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `className` | `string` | - | Additional CSS classes |
All [Base UI NavigationMenu.Icon props](https://base-ui.com/react/components/navigation-menu) are forwarded via `...props`.
### navigationMenuTriggerStyle
A `cva` helper exported for use outside of the `` component (e.g., applying trigger styles to standalone links).
```tsx
import { navigationMenuTriggerStyle } from "@/components/ui/navigation-menu";
Documentation
;
```
## Accessibility
### Keyboard Interactions
| Key | Action |
| ------------ | ----------------------------------------------------------------- |
| `Space` | Opens the dropdown when a trigger is focused |
| `Enter` | Opens the dropdown when a trigger is focused, activates links |
| `ArrowDown` | Opens the dropdown from a trigger; moves focus within content |
| `ArrowUp` | Moves focus within content |
| `ArrowRight` | Moves focus to the next trigger in the list |
| `ArrowLeft` | Moves focus to the previous trigger in the list |
| `Home` | Moves focus to the first trigger |
| `End` | Moves focus to the last trigger |
| `Escape` | Closes the open dropdown and returns focus to the trigger |
| `Tab` | Moves focus out of the navigation menu, closing any open dropdown |
### ARIA Attributes
- The root element renders with `role="navigation"`.
- Each trigger renders with `aria-expanded` and `aria-haspopup` attributes.
- The content panel is linked to its trigger via `aria-controls`.
- Links render with `role="link"`.
- Active links can be marked with `data-active="true"` for `aria-current` behavior.
# NumberField
> A number input with increment/decrement controls built on Base UI
URL: https://prototyper-ui.com/docs/components/number-field
Base UI reference: https://base-ui.com/react/components/number-field
```tsx
import {
NumberField,
NumberFieldGroup,
NumberFieldInput,
NumberFieldSteppers,
} from "@/components/ui/number-field";
import { FieldLabel } from "@/components/ui/field";
export default function NumberFieldDemo() {
return (
Width
);
}
```
## Installation
```bash
pnpm dlx shadcn@latest add https://prototyper-ui.com/r/number-field.json
```
This will add the following files to your project:
- `components/ui/number-field.tsx`
> **Note:** This component depends on [Field](/docs/components/field). It will be installed automatically.
## Usage
```tsx
import {
NumberField,
NumberFieldGroup,
NumberFieldInput,
NumberFieldSteppers,
} from "@/components/ui/number-field";
;
```
## Anatomy
```tsx
```
Or with individual step buttons:
```tsx
```
| Sub-component | `data-slot` | Purpose | Required |
| ---------------------------- | -------------------------------- | -------------------------------------------- | -------- |
| `NumberField` | `number-field` | Root provider, manages value state | Yes |
| `NumberFieldGroup` | `number-field-group` | Visual container for input and steppers | Yes |
| `NumberFieldInput` | `number-field-input` | The numeric input element | Yes |
| `NumberFieldIncrement` | `number-field-increment` | Button to increase the value | No |
| `NumberFieldDecrement` | `number-field-decrement` | Button to decrease the value | No |
| `NumberFieldSteppers` | `number-field-steppers` | Convenience wrapper with increment/decrement | No |
| `NumberFieldScrubArea` | `number-field-scrub-area` | Drag-to-scrub area for value adjustment | No |
| `NumberFieldScrubAreaCursor` | `number-field-scrub-area-cursor` | Custom cursor for the scrub area | No |
## Examples
### Currency
```tsx
import {
NumberField,
NumberFieldGroup,
NumberFieldInput,
NumberFieldSteppers,
} from "@/components/ui/number-field";
import { FieldLabel } from "@/components/ui/field";
export default function NumberFieldCurrency() {
return (
Transaction amount
);
}
```
### Description
```tsx
import {
NumberField,
NumberFieldGroup,
NumberFieldInput,
NumberFieldSteppers,
} from "@/components/ui/number-field";
import { FieldLabel } from "@/components/ui/field";
export default function NumberFieldDescription() {
return (
Width
Enter a width in centimeters.
);
}
```
### Disabled
```tsx
import {
NumberField,
NumberFieldGroup,
NumberFieldInput,
NumberFieldSteppers,
} from "@/components/ui/number-field";
import { FieldLabel } from "@/components/ui/field";
export default function NumberFieldDisabled() {
return (
Disabled
);
}
```
### Formatting
```tsx
import {
NumberField,
NumberFieldGroup,
NumberFieldInput,
NumberFieldSteppers,
} from "@/components/ui/number-field";
import { FieldLabel } from "@/components/ui/field";
export default function NumberFieldFormatting() {
return (
Adjust exposure
);
}
```
### Percentages
```tsx
import {
NumberField,
NumberFieldGroup,
NumberFieldInput,
NumberFieldSteppers,
} from "@/components/ui/number-field";
import { FieldLabel } from "@/components/ui/field";
export default function NumberFieldPercentages() {
return (
Sales tax
);
}
```
### Read Only
```tsx
import {
NumberField,
NumberFieldGroup,
NumberFieldInput,
NumberFieldSteppers,
} from "@/components/ui/number-field";
import { FieldLabel } from "@/components/ui/field";
export default function NumberFieldReadonly() {
return (
Read only
);
}
```
### Reusable
```tsx
import {
NumberField,
NumberFieldGroup,
NumberFieldInput,
NumberFieldSteppers,
} from "@/components/ui/number-field";
import { FieldDescription, FieldLabel } from "@/components/ui/field";
export default function NumberfieldReusable() {
return (
Cookies
Please enter a number
);
}
```
### Step Values
```tsx
import {
NumberField,
NumberFieldGroup,
NumberFieldInput,
NumberFieldSteppers,
} from "@/components/ui/number-field";
import { FieldLabel } from "@/components/ui/field";
export default function NumberFieldStepValues() {
return (
Step
Step + minValue
Step + minValue + maxValue
);
}
```
### Units
```tsx
import {
NumberField,
NumberFieldGroup,
NumberFieldInput,
NumberFieldSteppers,
} from "@/components/ui/number-field";
import { FieldLabel } from "@/components/ui/field";
export default function NumberFieldUnits() {
return (
Package width
);
}
```
### Validation
```tsx
import {
NumberField,
NumberFieldGroup,
NumberFieldInput,
NumberFieldSteppers,
} from "@/components/ui/number-field";
import { FieldLabel } from "@/components/ui/field";
export default function NumberFieldValidation() {
return (
Enter your age
);
}
```
### Validation Error
```tsx
import {
NumberField,
NumberFieldGroup,
NumberFieldInput,
NumberFieldSteppers,
} from "@/components/ui/number-field";
import { Button } from "@/components/ui/button";
import { FieldError, FieldLabel } from "@/components/ui/field";
export default function NumberFieldValidationError() {
return (
Width
Submit
);
}
```
## Styling
### Data Slots
Use `data-slot` attributes to target specific parts of the number field:
| Slot name | Element |
| -------------------------------- | ------------------------------------------ |
| `number-field` | Root wrapper |
| `number-field-group` | Visual container for input and controls |
| `number-field-input` | The ` ` element |
| `number-field-increment` | Increment button |
| `number-field-decrement` | Decrement button |
| `number-field-steppers` | Steppers container (increment + decrement) |
| `number-field-scrub-area` | Drag-to-scrub interaction area |
| `number-field-scrub-area-cursor` | Custom cursor for scrub area |
### Customization Examples
```css
/* Make the number field group larger */
[data-slot="number-field-group"] {
@apply h-12;
}
/* Style the stepper buttons */
[data-slot="number-field-increment"],
[data-slot="number-field-decrement"] {
@apply px-2 text-primary;
}
```
```tsx
{
/* Override size via className on the group */
}
;
```
## API Reference
### NumberField
Root component that manages the number field state. Built on Base UI `NumberField.Root`.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `className` | `string` | - | Additional CSS classes |
All [Base UI NumberField props](https://base-ui.com/react/components/number-field) are forwarded via `...props`.
### NumberFieldGroup
Visual container that wraps the input and stepper controls.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `size` | `"sm" \| "default" \| "lg"` | `"default"` | Height of the group container |
| `className` | `string` | - | Additional CSS classes |
All [Base UI NumberField.Group props](https://base-ui.com/react/components/number-field) are forwarded via `...props`.
### NumberFieldInput
The numeric input element.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `className` | `string` | - | Additional CSS classes |
All [Base UI NumberField.Input props](https://base-ui.com/react/components/number-field) are forwarded via `...props`.
### NumberFieldIncrement
Button that increments the value. Renders a chevron-up icon by default.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `className` | `string` | - | Additional CSS classes |
| `children` | `React.ReactNode` | - | Custom icon (replaces default chevron-up) |
All [Base UI NumberField.Increment props](https://base-ui.com/react/components/number-field) are forwarded via `...props`.
### NumberFieldDecrement
Button that decrements the value. Renders a chevron-down icon by default.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `className` | `string` | - | Additional CSS classes |
| `children` | `React.ReactNode` | - | Custom icon (replaces default chevron-down) |
All [Base UI NumberField.Decrement props](https://base-ui.com/react/components/number-field) are forwarded via `...props`.
### NumberFieldSteppers
Convenience wrapper that renders increment and decrement buttons in a vertical stack.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `className` | `string` | - | Additional CSS classes |
All standard `div` props are forwarded via `...props`.
### NumberFieldScrubArea
A drag-to-scrub interaction area for adjusting the value by dragging.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `className` | `string` | - | Additional CSS classes |
All [Base UI NumberField.ScrubArea props](https://base-ui.com/react/components/number-field) are forwarded via `...props`.
### NumberFieldScrubAreaCursor
Custom cursor element rendered within the scrub area.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `className` | `string` | - | Additional CSS classes |
All [Base UI NumberField.ScrubAreaCursor props](https://base-ui.com/react/components/number-field) are forwarded via `...props`.
### numberFieldGroupVariants
A `cva` helper exported for applying number field group styles outside the component.
```tsx
import { numberFieldGroupVariants } from "@/components/ui/number-field";
...
;
```
## Accessibility
### Keyboard Interactions
| Key | Action |
| ----------- | ----------------------------------------------- |
| `ArrowUp` | Increments the value by one step |
| `ArrowDown` | Decrements the value by one step |
| `Home` | Sets the value to the minimum (if `min` is set) |
| `End` | Sets the value to the maximum (if `max` is set) |
| `Tab` | Moves focus into or out of the number field |
### ARIA Attributes
- The input renders with `role="spinbutton"` via Base UI.
- `aria-valuenow`, `aria-valuemin`, and `aria-valuemax` are set automatically based on the current value and constraints.
- `aria-invalid` is set when validation fails.
- `data-disabled` is set on the group and controls when the field is disabled.
- Increment and decrement buttons are labeled for screen readers.
# Popover
> A popup anchored to a trigger element built on Base UI
URL: https://prototyper-ui.com/docs/components/popover
Base UI reference: https://base-ui.com/react/components/popover
```tsx
import { Button } from "@/components/ui/button";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { Switch } from "@/components/ui/switch";
export default function PopoverDemo() {
return (
}>
Settings
Wi-Fi
Bluetooth
Mute
);
}
```
## Installation
```bash
pnpm dlx shadcn@latest add https://prototyper-ui.com/r/popover.json
```
This will add the following files to your project:
- `components/ui/popover.tsx`
## Usage
```tsx
import {
Popover,
PopoverTrigger,
PopoverContent,
} from "@/components/ui/popover";
Open
Popover content here.
;
```
## Anatomy
```tsx
{/* your content */}
```
| Sub-component | `data-slot` | Purpose | Required |
| -------------------- | --------------------- | ---------------------------------------- | -------- |
| `Popover` | `popover` | Root provider, manages open/close state | Yes |
| `PopoverTrigger` | `popover-trigger` | Element that toggles the popover | Yes |
| `PopoverContent` | `popover-content` | The popup panel anchored to the trigger | Yes |
| `PopoverHeader` | `popover-header` | Flex container for title and description | No |
| `PopoverTitle` | `popover-title` | Accessible title for the popover | No |
| `PopoverDescription` | `popover-description` | Accessible description for the popover | No |
## Examples
### Container Padding
```tsx
import { Button } from "@/components/ui/button";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
export default function PopoverContainerPadding() {
return (
}>
Container Padding
This is a popover.
);
}
```
### Cross Offset
```tsx
import { Button } from "@/components/ui/button";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
export default function PopoverCrossOffset() {
return (
}>
Cross offset
Offset by an additional 100px.
);
}
```
### Flipping
```tsx
import { Button } from "@/components/ui/button";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
export default function PopoverFlipping() {
return (
}>
Default
This is a popover that will flip if it can't fully render below the
button.
}>
shouldFlip=false
This is a popover that won't flip if it can't fully render below the
button.
);
}
```
### Offset
```tsx
import { Button } from "@/components/ui/button";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
export default function PopoverOffset() {
return (
}>
Offset
Offset by an additional 50px.
);
}
```
### Position
```tsx
import { Button } from "@/components/ui/button";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
export default function PopoverPosition() {
return (
}>
⬅️
In left-to-right, this is on the left. In right-to-left, this is on
the right.
}>
⬆️
This popover is above the button.
}>
⬇️
This popover is below the button.
}>
➡️
In left-to-right, this is on the right. In right-to-left, this is on
the left.
);
}
```
## Styling
### Data Slots
Use `data-slot` attributes to target specific parts of the popover:
| Slot name | Element |
| --------------------- | ------------------------------- |
| `popover` | Root provider (no DOM rendered) |
| `popover-trigger` | The trigger element |
| `popover-content` | The popup panel |
| `popover-header` | Header container |
| `popover-title` | Title heading |
| `popover-description` | Description text |
### Customization Examples
```css
/* Make popover wider */
[data-slot="popover-content"] {
@apply min-w-[16rem];
}
/* Style the popover title */
[data-slot="popover-title"] {
@apply text-base font-bold;
}
```
```tsx
{
/* Override styles via className */
}
Settings
{/* ... */}
;
```
## API Reference
### Popover
Root component that manages open/close state.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `open` | `boolean` | - | Controlled open state |
| `onOpenChange` | `(open: boolean) => void` | - | Callback when open state changes |
| `defaultOpen` | `boolean` | `false` | Initial open state for uncontrolled usage |
All [Base UI Popover.Root props](https://base-ui.com/react/components/popover) are forwarded via `...props`.
### PopoverTrigger
Element that toggles the popover when clicked.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `className` | `string` | - | Additional CSS classes |
All [Base UI Popover.Trigger props](https://base-ui.com/react/components/popover) are forwarded via `...props`.
### PopoverContent
The popup panel rendered inside a portal with a positioner.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `align` | `"start" \| "center" \| "end"` | `"center"` | Alignment relative to the trigger |
| `alignOffset` | `number` | `0` | Offset from the alignment edge |
| `side` | `"top" \| "bottom" \| "left" \| "right"` | `"bottom"` | Preferred side relative to trigger |
| `sideOffset` | `number` | `4` | Gap between trigger and popup |
| `className` | `string` | - | Additional CSS classes |
| `children` | `React.ReactNode` | - | Popover content |
All [Base UI Popover.Popup props](https://base-ui.com/react/components/popover) are forwarded via `...props`.
### PopoverHeader
Flex column container for title and description.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `className` | `string` | - | Additional CSS classes |
| `children` | `React.ReactNode` | - | Header content |
Standard div props are forwarded via `...props`.
### PopoverTitle
Accessible title for the popover.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `className` | `string` | - | Additional CSS classes |
All [Base UI Popover.Title props](https://base-ui.com/react/components/popover) are forwarded via `...props`.
### PopoverDescription
Accessible description for the popover.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `className` | `string` | - | Additional CSS classes |
All [Base UI Popover.Description props](https://base-ui.com/react/components/popover) are forwarded via `...props`.
## Accessibility
### Keyboard Interactions
| Key | Action |
| ----------- | ---------------------------------------------------------------- |
| `Escape` | Closes the popover |
| `Tab` | Moves focus to the next focusable element within the popover |
| `Shift+Tab` | Moves focus to the previous focusable element within the popover |
### ARIA Attributes
- `PopoverContent` receives `role="dialog"` by default via Base UI.
- `aria-labelledby` is automatically linked to `PopoverTitle` when present.
- `aria-describedby` is automatically linked to `PopoverDescription` when present.
- Focus moves into the popover when it opens.
- Focus returns to the trigger element when the popover closes.
# Hover Card
> popup showing content preview on link hover
URL: https://prototyper-ui.com/docs/components/preview-card
Base UI reference: https://base-ui.com/react/components/preview-card
```tsx
"use client";
import {
HoverCard,
HoverCardContent,
HoverCardTrigger,
} from "@/components/ui/preview-card";
export default function PreviewCardDemo() {
return (
@base-ui
Base UI
Unstyled React components for building accessible UIs. Created and
maintained by MUI.
);
}
```
## Installation
```bash
pnpm dlx shadcn@latest add https://prototyper-ui.com/r/preview-card.json
```
This will add the following files to your project:
- `components/ui/preview-card.tsx`
## Usage
```tsx
import {
HoverCard,
HoverCardTrigger,
HoverCardContent,
} from "@/components/ui/preview-card";
@example
Preview content here.
;
```
## Anatomy
```tsx
{/* your preview content */}
```
| Sub-component | `data-slot` | Purpose | Required |
| ------------------ | -------------------- | ----------------------------------------- | -------- |
| `HoverCard` | `hover-card` | Root provider, manages open/close state | Yes |
| `HoverCardTrigger` | `hover-card-trigger` | Link element that shows the card on hover | Yes |
| `HoverCardContent` | `hover-card-content` | The popup panel anchored to the trigger | Yes |
## Styling
### Data Slots
Use `data-slot` attributes to target specific parts of the hover card:
| Slot name | Element |
| -------------------- | ------------------------------- |
| `hover-card` | Root provider (no DOM rendered) |
| `hover-card-trigger` | The trigger link element |
| `hover-card-portal` | Portal container |
| `hover-card-content` | The popup panel |
### Customization Examples
```css
/* Make hover card wider */
[data-slot="hover-card-content"] {
@apply w-80;
}
/* Style the trigger link */
[data-slot="hover-card-trigger"] {
@apply underline decoration-foreground/30 underline-offset-4;
}
```
```tsx
{
/* Override styles via className */
}
{/* Rich preview content */}
;
```
## API Reference
### HoverCard
Root component that manages open/close state. The card opens when the trigger is hovered.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `open` | `boolean` | - | Controlled open state |
| `onOpenChange` | `(open: boolean) => void` | - | Callback when open state changes |
| `defaultOpen` | `boolean` | `false` | Initial open state for uncontrolled usage |
All [Base UI PreviewCard.Root props](https://base-ui.com/react/components/preview-card) are forwarded via `...props`.
### HoverCardTrigger
The link element that shows the hover card when hovered. Renders an `` element by default.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `href` | `string` | - | URL the link points to |
| `className` | `string` | - | Additional CSS classes |
All [Base UI PreviewCard.Trigger props](https://base-ui.com/react/components/preview-card) are forwarded via `...props`.
### HoverCardContent
The popup panel rendered inside a portal with a positioner.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `align` | `"start" \| "center" \| "end"` | `"center"` | Alignment relative to the trigger |
| `alignOffset` | `number` | `4` | Offset from the alignment edge |
| `side` | `"top" \| "bottom" \| "left" \| "right"` | `"bottom"` | Preferred side relative to trigger |
| `sideOffset` | `number` | `4` | Gap between trigger and popup |
| `className` | `string` | - | Additional CSS classes |
| `children` | `React.ReactNode` | - | Hover card content |
All [Base UI PreviewCard.Popup props](https://base-ui.com/react/components/preview-card) are forwarded via `...props`.
## Accessibility
### Keyboard Interactions
| Key | Action |
| -------- | --------------------- |
| `Escape` | Closes the hover card |
### ARIA Attributes
- The trigger renders as an ` ` element, providing native link semantics.
- The hover card is intended for sighted users as a progressive enhancement -- the link remains functional without the card.
- Content inside the card is not announced to screen readers unless focused.
- The card opens on hover with a default delay of 600ms and closes with a 300ms delay.
# Progress
> A progress bar showing completion status
URL: https://prototyper-ui.com/docs/components/progress
Base UI reference: https://base-ui.com/react/components/progress
```tsx
"use client";
import React from "react";
import { Progress, ProgressLabel, ProgressValue } from "@/components/ui/progress";
export default function ProgressDemo() {
const [progress, setProgress] = React.useState(13);
React.useEffect(() => {
const timer = setTimeout(() => setProgress(80), 500);
return () => clearTimeout(timer);
}, []);
return (
);
}
```
## Installation
```bash
pnpm dlx shadcn@latest add https://prototyper-ui.com/r/progress.json
```
This will add the following files to your project:
- `components/ui/progress.tsx`
## Usage
```tsx
import { Progress, ProgressLabel } from "@/components/ui/progress";
Loading...
;
```
## Anatomy
```tsx
{/* ProgressTrack and ProgressIndicator are rendered internally by Progress */}
```
| Sub-component | `data-slot` | Purpose | Required |
| ------------------- | -------------------- | ------------------------------------------- | -------- |
| `Progress` | `progress` | Root provider with built-in track/indicator | Yes |
| `ProgressTrack` | `progress-track` | Background track for the indicator | Yes |
| `ProgressIndicator` | `progress-indicator` | Filled portion representing progress | Yes |
| `ProgressLabel` | `progress-label` | Accessible label for the progress bar | No |
| `ProgressValue` | `progress-value` | Displays the current value as text | No |
> **Note:** `ProgressTrack` and `ProgressIndicator` are rendered automatically inside the `Progress` component. You do not need to include them manually.
## Examples
### Custom Format
```tsx
import { Progress, ProgressLabel } from "@/components/ui/progress";
export default function ProgressCustomFormat() {
return (
Feeding...
30 of 100 dogs
);
}
```
### Reusable
```tsx
import { Progress, ProgressLabel, ProgressValue } from "@/components/ui/progress";
export default function ProgressReusable() {
return (
);
}
```
### Value Format
```tsx
import { Progress, ProgressLabel, ProgressValue } from "@/components/ui/progress";
export default function ProgressValueFormat() {
return (
);
}
```
## Styling
### Data Slots
Use `data-slot` attributes to target specific parts of the progress bar:
| Slot name | Element |
| -------------------- | -------------------- |
| `progress` | Root wrapper |
| `progress-track` | Background track bar |
| `progress-indicator` | Filled indicator bar |
| `progress-label` | Label text |
| `progress-value` | Value text |
### Customization Examples
```css
/* Make the progress track taller */
[data-slot="progress-track"] {
@apply h-4 rounded-lg;
}
/* Custom indicator with gradient */
[data-slot="progress-indicator"] {
@apply bg-gradient-to-r from-green-500 to-emerald-500;
}
```
```tsx
{
/* Override color via the color prop */
}
Upload
;
```
## API Reference
### Progress
Root component that provides progress value context and renders the track and indicator internally.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `value` | `number \| null` | - | Current progress value, null for indeterminate |
| `min` | `number` | `0` | Minimum value |
| `max` | `number` | `100` | Maximum value |
| `color` | `"default" \| "success" \| "warning" \| "destructive"` | `"default"` | Color variant for the track and indicator |
| `className` | `string` | - | Additional CSS classes |
| `children` | `React.ReactNode` | - | Progress content (label, value) |
All [Base UI Progress.Root props](https://base-ui.com/react/components/progress) are forwarded via `...props`.
### ProgressTrack
Background track that contains the indicator. Rendered automatically by `Progress`.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `color` | `"default" \| "success" \| "warning" \| "destructive"` | `"default"` | Color variant for the track |
| `size` | `"sm" \| "md" \| "lg" \| "xl"` | `"md"` | Size variant for the track height |
| `className` | `string` | - | Additional CSS classes |
| `children` | `React.ReactNode` | - | Track content (typically ProgressIndicator) |
All [Base UI Progress.Track props](https://base-ui.com/react/components/progress) are forwarded via `...props`.
### ProgressIndicator
The filled portion of the track representing current progress. Rendered automatically by `Progress`.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `color` | `"default" \| "success" \| "warning" \| "destructive"` | `"default"` | Color variant for the indicator |
| `className` | `string` | - | Additional CSS classes |
All [Base UI Progress.Indicator props](https://base-ui.com/react/components/progress) are forwarded via `...props`.
### ProgressLabel
Accessible label for the progress bar.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `className` | `string` | - | Additional CSS classes |
| `children` | `React.ReactNode` | - | Label text |
All [Base UI Progress.Label props](https://base-ui.com/react/components/progress) are forwarded via `...props`.
### ProgressValue
Displays the current progress value as formatted text.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `className` | `string` | - | Additional CSS classes |
| `children` | `React.ReactNode` | - | Custom value rendering |
All [Base UI Progress.Value props](https://base-ui.com/react/components/progress) are forwarded via `...props`.
### progressTrackVariants
A `cva` helper exported for applying progress track styles outside of the `` component.
```tsx
import { progressTrackVariants } from "@/components/ui/progress";
Custom track
;
```
### progressIndicatorVariants
A `cva` helper exported for applying progress indicator styles outside of the `` component.
```tsx
import { progressIndicatorVariants } from "@/components/ui/progress";
Custom indicator
;
```
## Accessibility
### Keyboard Interactions
The progress bar is a non-interactive display element and does not have keyboard interactions. Focus management is handled by surrounding interactive elements.
### ARIA Attributes
- The progress bar renders with `role="progressbar"` via the Base UI primitive.
- `aria-valuenow` reflects the current value.
- `aria-valuemin` and `aria-valuemax` define the range.
- When `value` is `null`, the progress bar enters indeterminate mode and `aria-valuenow` is removed.
- `aria-labelledby` is automatically linked to `ProgressLabel` when present.
- The `data-indeterminate` attribute is set when value is `null`, and `data-complete` is set when value equals max.
- Screen readers announce the progress value and its label.
## Compose
This component is available in [`@prototyperco/compose`](/docs/compose).
### Catalog Definition
```typescript title="progress.catalog.ts"
import { z } from "zod";
import { defineComponent } from "@prototyperco/compose/catalog";
export default defineComponent({
description: "A progress bar indicating completion status",
props: z.object({
value: z.number().min(0).max(100).describe("Progress value from 0 to 100"),
label: z
.string()
.optional()
.describe("Label displayed above the progress bar"),
color: z
.enum(["default", "success", "warning", "destructive", "info"])
.optional()
.describe("Color variant of the progress fill"),
size: z
.enum(["sm", "md", "lg", "xl"])
.optional()
.describe("Height of the progress bar"),
}),
example: {
value: 65,
label: "Upload progress",
color: "default",
size: "md",
},
});
```
### Example Spec
```json
{
"root": "progress",
"elements": {
"progress": {
"type": "Progress",
"props": {
"value": 65,
"label": "Upload progress",
"color": "default",
"size": "md"
}
}
}
}
```
Learn more in the [Compose documentation](/docs/compose).
# Radio Group
> A group of radio buttons built on Base UI
URL: https://prototyper-ui.com/docs/components/radio-group
Base UI reference: https://base-ui.com/react/components/radio
```tsx
import { FieldLabel } from "@/components/ui/field";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
export default function RadioGroupDemo() {
return (
Favorite pet
Dog
Cat
Dragon
);
}
```
## Installation
```bash
pnpm dlx shadcn@latest add https://prototyper-ui.com/r/radio-group.json
```
This will add the following files to your project:
- `components/ui/radio-group.tsx`
## Usage
```tsx
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
Option 1
Option 2
Option 3
;
```
## Anatomy
```tsx
```
| Sub-component | `data-slot` | Purpose | Required |
| ---------------- | ----------------------- | ---------------------------------------------------------------------------- | -------- |
| `RadioGroup` | `radio-group` | Root container, manages selection state | Yes |
| `RadioGroupItem` | `radio-group-item` | Individual radio button with built-in indicator | Yes |
| Indicator | `radio-group-indicator` | Selection dot (internal element managed by `RadioGroupItem`, not composable) | — |
## Examples
### Description
```tsx
import { FieldLabel } from "@/components/ui/field";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
export default function RadioGroupDescription() {
return (
Favorite avatar
Wizard
Dragon
Please select an avatar.
);
}
```
### Disabled
```tsx
import { FieldLabel } from "@/components/ui/field";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
export default function RadioGroupDisabled() {
return (
Favorite sport
Soccer
Baseball
Basketball
);
}
```
### Disabled Individual
```tsx
import { FieldLabel } from "@/components/ui/field";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
export default function RadioGroupDisabledIndividual() {
return (
Favorite sport
Soccer
Baseball
Basketball
);
}
```
### Orientation
```tsx
import { FieldLabel } from "@/components/ui/field";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
export default function RadioGroupOrientation() {
return (
Favorite avatar
Wizard
Dragon
);
}
```
### Read Only
```tsx
import { FieldLabel } from "@/components/ui/field";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
export default function RadioGroupReadonly() {
return (
Favorite avatar
Wizard
Dragon
);
}
```
### Reusable
```tsx
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { FieldLabel } from "@/components/ui/field";
export default function RadioGroupReusable() {
return (
Favorite sport *
Soccer
Baseball
Basketball
Select a favorite sport
);
}
```
### Validation
```tsx
import { Button } from "@/components/ui/button";
import { FieldError, FieldLabel } from "@/components/ui/field";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
export default function RadioGroupValidation() {
return (
Favorite pet
Dog
Cat
Dragon
Submit
);
}
```
## Styling
### Data Slots
Use `data-slot` attributes to target specific parts of the radio group:
| Slot name | Element |
| ----------------------- | ----------------------------------- |
| `radio-group` | Root container for all radio items |
| `radio-group-item` | Individual radio button circle |
| `radio-group-indicator` | The selection dot inside the circle |
### Customization Examples
```css
/* Make radio items larger */
[data-slot="radio-group-item"] {
@apply size-5;
}
/* Custom checked color */
[data-slot="radio-group-item"] {
@apply data-checked:bg-green-600 data-checked:border-green-600;
}
```
```tsx
{
/* Override layout via className */
}
A
B
;
```
## API Reference
### RadioGroup
Root container that manages which radio item is selected.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `value` | `string` | - | Controlled selected value |
| `defaultValue` | `string` | - | Initial selected value for uncontrolled usage |
| `onValueChange` | `(value: string, event: Event) => void` | - | Callback when selection changes |
| `disabled` | `boolean` | `false` | Whether all radio items are disabled |
| `readOnly` | `boolean` | `false` | Whether the group is read-only |
| `required` | `boolean` | `false` | Whether a selection is required |
| `name` | `string` | - | Name attribute for form submission |
| `orientation` | `"horizontal" \| "vertical"` | `"vertical"` | Orientation of the radio group |
| `className` | `string` | - | Additional CSS classes |
All [Base UI RadioGroup props](https://base-ui.com/react/components/radio) are forwarded via `...props`.
### RadioGroupItem
An individual radio button with a built-in selection indicator.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `value` | `string` | - | Unique value for this radio item (required) |
| `disabled` | `boolean` | `false` | Whether this individual item is disabled |
| `className` | `string` | - | Additional CSS classes |
All [Base UI Radio props](https://base-ui.com/react/components/radio) are forwarded via `...props`.
## Accessibility
### Keyboard Interactions
| Key | Action |
| ------------ | ---------------------------------------------------- |
| `ArrowDown` | Moves focus and selection to the next radio item |
| `ArrowRight` | Moves focus and selection to the next radio item |
| `ArrowUp` | Moves focus and selection to the previous radio item |
| `ArrowLeft` | Moves focus and selection to the previous radio item |
| `Tab` | Moves focus into / out of the radio group |
### ARIA Attributes
- `RadioGroup` renders with `role="radiogroup"` via Base UI.
- Each `RadioGroupItem` renders with `role="radio"`.
- `aria-checked` is set to `true` on the selected item and `false` on others.
- `aria-disabled` is set when the group or an individual item is disabled.
- `aria-readonly` is set when the group is read-only.
- `aria-required` is set when the group is required.
- Screen readers announce each radio item label and its selected/unselected state.
## Compose
This component is available in [`@prototyperco/compose`](/docs/compose).
### Catalog Definition
```typescript title="radio-group.catalog.ts"
import { z } from "zod";
import { defineComponent } from "@prototyperco/compose/catalog";
export default defineComponent({
description:
"A group of radio buttons for selecting a single option from a list",
props: z.object({
label: z.string().optional().describe("Label for the radio group fieldset"),
options: z
.array(
z.object({
label: z.string().describe("Display label for the option"),
value: z.string().describe("Value of the option"),
}),
)
.describe("Available radio options"),
value: z
.string()
.optional()
.describe("Currently selected value (bindable)"),
orientation: z
.enum(["vertical", "horizontal"])
.optional()
.describe("Layout direction of the radio buttons"),
disabled: z
.boolean()
.optional()
.describe("Whether the radio group is disabled"),
}),
events: ["change"],
example: {
label: "Select a plan",
options: [
{ label: "Free", value: "free" },
{ label: "Pro", value: "pro" },
{ label: "Enterprise", value: "enterprise" },
],
value: "free",
},
});
```
### Example Spec
```json
{
"root": "planPicker",
"elements": {
"planPicker": {
"type": "RadioGroup",
"props": {
"label": "Select a plan",
"options": [
{ "label": "Free", "value": "free" },
{ "label": "Pro", "value": "pro" },
{ "label": "Enterprise", "value": "enterprise" }
],
"value": "free"
},
"$bindState": { "path": "/selectedPlan", "event": "change" }
}
}
}
```
Learn more in the [Compose documentation](/docs/compose).
# Resizable Panel
> Resizable split panels with drag handles for flexible layouts
URL: https://prototyper-ui.com/docs/components/resizable-panel
```tsx
import {
ResizablePanelGroup,
ResizablePanel,
ResizableHandle,
} from "@/components/ui/resizable-panel";
export default function ResizablePanelDemo() {
return (
Panel A
Panel B
);
}
```
## Installation
```bash
pnpm dlx shadcn@latest add https://prototyper-ui.com/r/resizable-panel.json
```
This will add the following files to your project:
- `components/ui/resizable-panel.tsx`
### Dependencies
This component uses [`react-resizable-panels`](https://github.com/bvaughn/react-resizable-panels) under the hood. It will be installed automatically.
## Usage
```tsx
import {
ResizablePanelGroup,
ResizablePanel,
ResizableHandle,
} from "@/components/ui/resizable-panel";
Left panel content
Right panel content
;
```
## Anatomy
```tsx
```
| Sub-component | `data-slot` | Purpose | Required |
| --------------------- | ----------------------- | ------------------------------------ | -------- |
| `ResizablePanelGroup` | `resizable-panel-group` | Container that manages panel layout | Yes |
| `ResizablePanel` | `resizable-panel` | Individual panel with resizable size | Yes |
| `ResizableHandle` | `resizable-handle` | Drag handle between panels | Yes |
## Examples
### Vertical orientation
```tsx
Top
Bottom
```
### Three panels
```tsx
Sidebar
Main
Inspector
```
## Styling
### Data Slots
| Slot name | Element |
| ----------------------- | ----------------------------- |
| `resizable-panel-group` | Flex container for all panels |
| `resizable-panel` | Individual panel |
| `resizable-handle` | Drag handle separator |
### Customization Examples
```css
/* Custom handle color */
[data-slot="resizable-handle"] {
@apply bg-primary/20;
}
/* Wider drag zone */
[data-slot="resizable-handle"]::after {
left: -4px;
right: -4px;
}
```
## API Reference
### ResizablePanelGroup
Container that manages layout of child panels. Wraps `react-resizable-panels` `Group`.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `orientation` | `"horizontal" \| "vertical"` | `"horizontal"` | Panel layout direction |
| `className` | `string` | - | Additional CSS classes |
All [`react-resizable-panels` Group props](https://github.com/bvaughn/react-resizable-panels) are forwarded.
### ResizablePanel
Individual resizable panel. Wraps `react-resizable-panels` `Panel`.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `defaultSize` | `string` | - | Initial size as percentage (e.g. "50%") |
| `minSize` | `string` | - | Minimum size as percentage |
| `maxSize` | `string` | - | Maximum size as percentage |
| `className` | `string` | - | Additional CSS classes |
All [`react-resizable-panels` Panel props](https://github.com/bvaughn/react-resizable-panels) are forwarded.
### ResizableHandle
Drag handle between panels. Wraps `react-resizable-panels` `Separator`.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `withHandle` | `boolean` | `false` | Show a visual grip icon on the handle |
| `className` | `string` | - | Additional CSS classes |
All [`react-resizable-panels` Separator props](https://github.com/bvaughn/react-resizable-panels) are forwarded.
## Accessibility
### Keyboard Interactions
| Key | Action |
| ------------ | ------------------------------------------- |
| `ArrowLeft` | Shrinks the panel to the left of the handle |
| `ArrowRight` | Grows the panel to the left of the handle |
| `ArrowUp` | Shrinks the panel above the handle |
| `ArrowDown` | Grows the panel above the handle |
| `Home` | Collapses the panel to its minimum size |
| `End` | Expands the panel to its maximum size |
Keyboard interactions are provided by the underlying `react-resizable-panels` library.
# Row
> A horizontal flex layout with gap, alignment, and justify options.
URL: https://prototyper-ui.com/docs/components/row
```tsx
import { Row } from "@/components/ui/row";
export default function RowDemo() {
return (
Item 1
Item 2
Item 3
);
}
```
## Installation
```bash
pnpm dlx shadcn@latest add https://prototyper-ui.com/r/row.json
```
This will add the following files to your project:
- `components/ui/row.tsx`
## Usage
```tsx
import { Row } from "@/components/ui/row";
```
```tsx
Left
Right
```
Row is a horizontal flex container with props for `gap`, `align`, `justify`, and `wrap`. Use it for toolbars, header layouts, button groups, and tag lists.
## Examples
### Space Between
A common header pattern with logo on the left and navigation on the right.
```tsx
import { Row } from "@/components/ui/row";
export default function RowBetween() {
return (
Logo
Docs
Pricing
Blog
);
}
```
### Centered
Centered button group, useful for dialog footers and form actions.
```tsx
import { Row } from "@/components/ui/row";
import { Button } from "@/components/ui/button";
export default function RowCentered() {
return (
Cancel
Save Changes
);
}
```
### Wrapping
Wrapping row for tags, badges, or chips that overflow to the next line.
```tsx
import { Row } from "@/components/ui/row";
import { Badge } from "@/components/ui/badge";
export default function RowWrap() {
return (
React
TypeScript
Tailwind CSS
Base UI
Next.js
Turborepo
pnpm
Vitest
);
}
```
## Styling
### Data Slots
Use `data-slot` attributes to target the row in CSS:
| Slot name | Element |
| --------- | ---------------- |
| `row` | The `` root |
### Customization Examples
```css
/* Add a bottom border to all rows */
[data-slot="row"] {
@apply border-b border-border pb-2;
}
```
```tsx
Custom styled row
```
## API Reference
### Row
A horizontal flex container with layout props.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `gap` | `number` | `3` | Gap between items. Maps to Tailwind gap utilities (0, 1, 2, 3, 4, 5, 6, 8, 10, 12). |
| `align` | `"start" \| "center" \| "end" \| "stretch" \| "baseline"` | `"center"` | Vertical alignment of items (items-*). |
| `justify` | `"start" \| "center" \| "end" \| "between" \| "around" \| "evenly"` | `"start"` | Horizontal distribution of items (justify-*). |
| `wrap` | `boolean` | `false` | Whether items wrap to the next line when they overflow. |
| `className` | `string` | - | Additional CSS classes. |
| `children` | `React.ReactNode` | - | Row content. |
Extends `React.ComponentProps<"div">`. All standard div props are forwarded via `...props`.
## Accessibility
### Keyboard Interactions
Row is a non-interactive layout container and does not have keyboard interactions.
### ARIA Attributes
- Renders as a plain `
` with no implicit ARIA role.
- No additional ARIA attributes are needed for a flex layout container.
# Scroll Area
> A scrollable container with custom styled scrollbars
URL: https://prototyper-ui.com/docs/components/scroll-area
Base UI reference: https://base-ui.com/react/components/scroll-area
```tsx
"use client";
import { ScrollArea } from "@/components/ui/scroll-area";
const tags = Array.from({ length: 50 }).map(
(_, i, a) => `v1.2.0-beta.${a.length - i}`,
);
export default function ScrollAreaDemo() {
return (
Tags
{tags.map((tag) => (
))}
);
}
```
## Installation
```bash
pnpm dlx shadcn@latest add https://prototyper-ui.com/r/scroll-area.json
```
This will add the following files to your project:
- `components/ui/scroll-area.tsx`
## Usage
```tsx
import { ScrollArea } from "@/components/ui/scroll-area";
{/* Long content here */}
;
```
## Anatomy
```tsx
{/* ScrollArea.Viewport, ScrollBar, and ScrollArea.Corner are rendered internally */}
Your scrollable content
```
| Sub-component | `data-slot` | Purpose | Required |
| ------------- | ----------------------- | ----------------------------------------------- | -------- |
| `ScrollArea` | `scroll-area` | Root container with built-in viewport/scrollbar | Yes |
| `ScrollBar` | `scroll-area-scrollbar` | Scrollbar track (vertical or horizontal) | Internal |
| Viewport | `scroll-area-viewport` | Scrollable viewport container | Internal |
| Thumb | `scroll-area-thumb` | Draggable scrollbar indicator | Internal |
> **Note:** The viewport, scrollbar, thumb, and corner are rendered automatically inside the `ScrollArea` component. You only need to provide child content.
## Styling
### Data Slots
Use `data-slot` attributes to target specific parts of the scroll area:
| Slot name | Element |
| ----------------------- | ------------------------- |
| `scroll-area` | Root wrapper |
| `scroll-area-viewport` | Scrollable viewport |
| `scroll-area-scrollbar` | Scrollbar track |
| `scroll-area-thumb` | Scrollbar thumb indicator |
### Customization Examples
```css
/* Make the scrollbar wider */
[data-slot="scroll-area-scrollbar"] {
@apply w-3.5;
}
/* Custom thumb color */
[data-slot="scroll-area-thumb"] {
@apply bg-primary/50;
}
```
```tsx
{
/* Add horizontal scrollbar with the ScrollBar component */
}
import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area";
{items.map((item) => (
{item}
))}
;
```
## API Reference
### ScrollArea
Root component that provides a scrollable viewport with custom scrollbars. Renders a vertical scrollbar by default.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `className` | `string` | - | Additional CSS classes for the root container |
| `children` | `React.ReactNode` | - | Scrollable content |
All [Base UI ScrollArea.Root props](https://base-ui.com/react/components/scroll-area) are forwarded via `...props`.
### ScrollBar
A scrollbar track with a draggable thumb. One vertical scrollbar is rendered by default inside `ScrollArea`. Use this component to add an additional horizontal scrollbar.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `orientation` | `"vertical" \| "horizontal"` | `"vertical"` | The axis the scrollbar controls |
| `className` | `string` | - | Additional CSS classes for the scrollbar |
All [Base UI ScrollArea.Scrollbar props](https://base-ui.com/react/components/scroll-area) are forwarded via `...props`.
## Accessibility
### Keyboard Interactions
| Key | Description |
| -------------------------- | ----------------------------------------------------- |
| `Tab` | Moves focus into the scroll area viewport |
| `ArrowUp` / `ArrowDown` | Scrolls content vertically when viewport is focused |
| `ArrowLeft` / `ArrowRight` | Scrolls content horizontally when viewport is focused |
| `PageUp` / `PageDown` | Scrolls content by one page |
| `Home` / `End` | Scrolls to the start or end of content |
### ARIA Attributes
- The viewport is focusable with `tabindex="0"` and includes a `focus-visible` ring for keyboard users.
- Scroll position is managed natively by the browser, providing standard scrolling behavior and screen reader compatibility.
- The custom scrollbars are decorative overlays; actual scrolling relies on native browser scroll mechanics.
- `data-has-overflow-x` and `data-has-overflow-y` attributes on the root indicate when content exceeds the viewport.
- `data-scrolling` is present on the root during active scrolling.
# Section
> A full-width page section with background variants and constrained content.
URL: https://prototyper-ui.com/docs/components/section
```tsx
import { Section } from "@/components/ui/section";
export default function SectionDemo() {
return (
<>
Hero Section
Default background, large padding
Features
Surface background, medium padding
Call to Action
Muted background, large padding
>
);
}
```
## Installation
```bash
pnpm dlx shadcn@latest add https://prototyper-ui.com/r/section.json
```
This will add the following files to your project:
- `components/ui/section.tsx`
## Usage
```tsx
import { Section } from "@/components/ui/section";
```
```tsx
Section heading
Section content is automatically constrained and centered.
```
Section wraps content in a full-bleed container with an optional background color. The inner content is automatically constrained to a `maxWidth` and centered with horizontal padding.
## Examples
### Primary Background
```tsx
import { Section } from "@/components/ui/section";
export default function SectionPrimary() {
return (
Get Started Today
Primary background section for high-impact call-to-action areas.
);
}
```
### Narrow Content
```tsx
import { Section } from "@/components/ui/section";
export default function SectionNarrow() {
return (
Focused Content
A narrower max-width keeps text readable for long-form content like
articles and blog posts.
);
}
```
## Styling
### Data Slots
Use `data-slot` attributes to target Section parts in CSS:
| Slot name | Element |
| ----------------- | ----------------------------- |
| `section` | The outer `` root |
| `section-content` | The inner constrained `` |
### Customization Examples
```css
/* Add a top border to surface sections */
[data-slot="section"] {
@apply border-t border-border;
}
/* Override inner content max-width */
[data-slot="section-content"] {
@apply max-w-3xl;
}
```
```tsx
```
## API Reference
### Section
A full-bleed section with constrained inner content.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `background` | `"none" \| "default" \| "surface" \| "surface-secondary" \| "surface-tertiary" \| "muted" \| "primary"` | `"none"` | Background color variant for the section. |
| `padding` | `"none" \| "sm" \| "md" \| "lg" \| "xl"` | `"lg"` | Vertical padding applied to the section. |
| `maxWidth` | `"sm" \| "md" \| "lg" \| "xl" \| "2xl" \| "4xl" \| "5xl" \| "full"` | `"5xl"` | Max-width constraint for the inner content. |
| `centered` | `boolean` | `true` | Whether to center the inner content with mx-auto. |
| `className` | `string` | - | Additional CSS classes applied to the outer section. |
| `children` | `React.ReactNode` | - | Section content. |
Extends `React.ComponentProps<"section">`. All standard section props are forwarded via `...props`.
### sectionVariants
A `cva` helper exported for use outside of the `
` component.
```tsx
import { sectionVariants } from "@/components/ui/section";
Custom element with section styles
;
```
## Accessibility
### Keyboard Interactions
Section is a non-interactive presentational container and does not have keyboard interactions.
### ARIA Attributes
- Renders as a native `` element, which is treated as a landmark by assistive technology when given an accessible name.
- Add `aria-label` or `aria-labelledby` to give the section a meaningful label for screen readers.
# Segmented Control
> a segmented control for switching between options built on Base UI
URL: https://prototyper-ui.com/docs/components/segmented-control
Base UI reference: https://base-ui.com/react/components/radio-group
```tsx
import {
SegmentedControl,
SegmentedControlItem,
} from "@/components/ui/segmented-control";
export default function SegmentedControlDemo() {
return (
Daily
Weekly
Monthly
);
}
```
## Installation
```bash
pnpm dlx shadcn@latest add https://prototyper-ui.com/r/segmented-control.json
```
This will add the following files to your project:
- `components/ui/segmented-control.tsx`
## Usage
```tsx
import {
SegmentedControl,
SegmentedControlItem,
} from "@/components/ui/segmented-control";
```
```tsx
Option 1
Option 2
Option 3
```
## Anatomy
```tsx
```
| Sub-component | `data-slot` | Purpose | Required |
| ---------------------- | ------------------------ | ------------------------------ | -------- |
| `SegmentedControl` | `segmented-control` | Root container and radio group | Yes |
| `SegmentedControlItem` | `segmented-control-item` | Individual selectable option | Yes |
## Examples
### Small
```tsx
import {
SegmentedControl,
SegmentedControlItem,
} from "@/components/ui/segmented-control";
export default function SegmentedControlSmall() {
return (
Grid
List
Table
);
}
```
## Styling
### Data Slots
Use `data-slot` attributes to target specific parts of the segmented control:
| Slot name | Element |
| ----------------------------- | -------------------------------- |
| `segmented-control` | Root container |
| `segmented-control-item` | Individual option button |
| `segmented-control-indicator` | Check indicator inside each item |
### Customization Examples
```css
/* Squared segmented control */
[data-slot="segmented-control"] {
@apply rounded-lg;
}
[data-slot="segmented-control-item"] {
@apply rounded-md;
}
```
```tsx
A
B
```
## API Reference
### SegmentedControl
Root container that manages selection state. Built on Base UI RadioGroup.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `size` | `"sm" \| "md"` | `"md"` | Size of the segmented control. |
| `value` | `string` | - | Controlled selected value. |
| `defaultValue` | `string` | - | Initial selected value for uncontrolled usage. |
| `onValueChange` | `(value: string, event: Event) => void` | - | Callback when the selected value changes. |
| `disabled` | `boolean` | `false` | Whether the entire control is disabled. |
| `className` | `string` | - | Additional CSS classes. |
| `children` | `React.ReactNode` | - | SegmentedControlItem elements. |
All [Base UI RadioGroup props](https://base-ui.com/react/components/radio-group) are forwarded via `...props`.
### SegmentedControlItem
An individual selectable option within the segmented control.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `value` | `string` | - | The value of this option. |
| `disabled` | `boolean` | `false` | Whether this item is disabled. |
| `className` | `string` | - | Additional CSS classes. |
| `children` | `React.ReactNode` | - | Item label content. |
All [Base UI Radio props](https://base-ui.com/react/components/radio-group) are forwarded via `...props`.
### segmentedControlVariants
A `cva` helper exported for applying segmented control styles outside the component.
```tsx
import { segmentedControlVariants } from "@/components/ui/segmented-control";
Custom element
;
```
## Accessibility
### Keyboard Interactions
| Key | Action |
| ------------ | ---------------------------------------------- |
| `Tab` | Moves focus to the segmented control |
| `ArrowRight` | Moves focus and selection to the next item |
| `ArrowLeft` | Moves focus and selection to the previous item |
| `Space` | Selects the focused item |
### ARIA Attributes
- The root renders with `role="radiogroup"` via Base UI.
- Each item renders with `role="radio"`.
- `aria-checked` is set to `true` on the selected item, `false` on others.
- `aria-disabled` is set when the control or individual items are disabled.
# Select
> A dropdown select input built on Base UI
URL: https://prototyper-ui.com/docs/components/select
Base UI reference: https://base-ui.com/react/components/select
```tsx
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
export default function SelectDemo() {
return (
Apple
Banana
Blueberry
Grapes
Pineapple
);
}
```
## Installation
```bash
pnpm dlx shadcn@latest add https://prototyper-ui.com/r/select.json
```
This will add the following files to your project:
- `components/ui/select.tsx`
## Usage
```tsx
import {
Select,
SelectTrigger,
SelectValue,
SelectContent,
SelectItem,
} from "@/components/ui/select";
Option 1
Option 2
;
```
## Anatomy
```tsx
```
| Sub-component | `data-slot` | Purpose | Required |
| ------------------------ | --------------------------- | ------------------------------------------ | -------- |
| `Select` | `select` | Root provider, manages selection state | Yes |
| `SelectTrigger` | `select-trigger` | Button that opens the dropdown | Yes |
| `SelectValue` | `select-value` | Displays the currently selected value | Yes |
| `SelectContent` | `select-content` | Popup container for items | Yes |
| `SelectItem` | `select-item` | An individual selectable option | Yes |
| `SelectGroup` | `select-group` | Groups related items together | No |
| `SelectLabel` | `select-label` | Label for a group of items | No |
| `SelectSeparator` | `select-separator` | Visual divider between items or groups | No |
| `SelectScrollUpButton` | `select-scroll-up-button` | Scroll indicator at the top of the list | No |
| `SelectScrollDownButton` | `select-scroll-down-button` | Scroll indicator at the bottom of the list | No |
## Examples
### Content
```tsx
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
export default function SelectContentDemo() {
const options = [
{ id: 1, name: "Aerospace" },
{ id: 2, name: "Mechanical" },
{ id: 3, name: "Civil" },
{ id: 4, name: "Biomedical" },
{ id: 5, name: "Nuclear" },
{ id: 6, name: "Industrial" },
{ id: 7, name: "Chemical" },
{ id: 8, name: "Agricultural" },
{ id: 9, name: "Electrical" },
];
return (
{options.map((item) => (
{item.name}
))}
);
}
```
### Description
```tsx
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
export default function SelectDescription() {
return (
Aardvark
Cat
Dog
Kangaroo
Panda
Snake
Please select an animal.
);
}
```
### Disabled
```tsx
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
export default function SelectDisabled() {
return (
Apple
Banana
Blueberry
);
}
```
### Disabled Items
```tsx
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
export default function SelectDisabledItems() {
return (
Apple
Banana (out of stock)
Blueberry
Grapes (out of stock)
Pineapple
);
}
```
### Links
```tsx
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
export default function SelectLinks() {
return (
Create new…
Proposal
Budget
Onboarding
);
}
```
### Reusable
```tsx
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
export default function SelectReusable() {
return (
Ice cream flavor
Chocolate
Mint
Strawberry
Vanilla
Select a flavor
);
}
```
### Sections
```tsx
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectSeparator,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
export default function SelectSections() {
return (
Fruits
Apple
Banana
Blueberry
Vegetables
Carrot
Potato
Tomato
);
}
```
### Sections Dynamic
```tsx
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
export default function SelectSectionsDynamic() {
const options = [
{
name: "Fruit",
children: [
{ name: "Apple" },
{ name: "Banana" },
{ name: "Orange" },
{ name: "Honeydew" },
{ name: "Grapes" },
{ name: "Watermelon" },
{ name: "Cantaloupe" },
{ name: "Pear" },
],
},
{
name: "Vegetable",
children: [
{ name: "Cabbage" },
{ name: "Broccoli" },
{ name: "Carrots" },
{ name: "Lettuce" },
{ name: "Spinach" },
{ name: "Bok Choy" },
{ name: "Cauliflower" },
{ name: "Potatoes" },
],
},
];
return (
{options.map((section) => (
{section.name}
{section.children.map((item) => (
{item.name}
))}
))}
);
}
```
### Text Slots
```tsx
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
export default function SelectTextSlots() {
return (
Read only
Read only
Write
Read and write only
Admin
Full access
);
}
```
### Validation
```tsx
"use client";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
export default function SelectValidation() {
const [value, setValue] = useState(null);
const [error, setError] = useState();
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (!value) {
setError("Please select an animal");
} else {
setError(undefined);
alert(`Selected: ${value}`);
}
};
return (
{
setValue(v);
setError(undefined);
}}
>
Aardvark
Cat
Dog
Kangaroo
Panda
Snake
{error &&
{error}
}
Submit
);
}
```
## Styling
### Data Slots
Use `data-slot` attributes to target specific parts of the select:
| Slot name | Element |
| --------------------------- | ------------------------------- |
| `select` | Root provider |
| `select-trigger` | The trigger button |
| `select-value` | Displayed selected value text |
| `select-content` | The dropdown popup panel |
| `select-item` | An individual option |
| `select-group` | Group wrapper for related items |
| `select-label` | Label for a group |
| `select-separator` | Visual divider |
| `select-scroll-up-button` | Top scroll indicator |
| `select-scroll-down-button` | Bottom scroll indicator |
### Customization Examples
```css
/* Make the trigger wider */
[data-slot="select-trigger"] {
@apply w-64;
}
/* Custom highlight color for items */
[data-slot="select-item"][data-highlighted] {
@apply bg-primary text-primary-foreground;
}
```
```tsx
{
/* Override trigger size via className */
}
;
```
## API Reference
### Select
Root component that manages selection state and context.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `defaultValue` | `any` | - | Initial value for uncontrolled usage |
| `value` | `any` | - | Controlled selected value |
| `onValueChange` | `(value: any) => void` | - | Callback when the selected value changes |
| `disabled` | `boolean` | `false` | Whether the select is disabled |
All [Base UI Select.Root props](https://base-ui.com/react/components/select) are forwarded via `...props`.
### SelectTrigger
Button that opens the dropdown when clicked.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `size` | `"default" \| "sm" \| "lg"` | `"default"` | Size of the trigger |
| `className` | `string` | - | Additional CSS classes |
| `children` | `React.ReactNode` | - | Trigger content |
All [Base UI Select.Trigger props](https://base-ui.com/react/components/select) are forwarded via `...props`.
### SelectValue
Displays the currently selected value or a placeholder.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `placeholder` | `string` | - | Text shown when no value is selected |
| `className` | `string` | - | Additional CSS classes |
All [Base UI Select.Value props](https://base-ui.com/react/components/select) are forwarded via `...props`.
### SelectContent
The dropdown popup panel rendered inside a portal with positioning.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `side` | `"top" \| "bottom" \| "left" \| "right"` | `"bottom"` | Preferred side of the trigger to render |
| `sideOffset` | `number` | `4` | Distance from the trigger in pixels |
| `align` | `"start" \| "center" \| "end"` | `"center"` | Alignment relative to the trigger |
| `alignOffset` | `number` | `0` | Offset from the alignment edge in pixels |
| `alignItemWithTrigger` | `boolean` | `true` | Align the selected item with the trigger |
| `className` | `string` | - | Additional CSS classes |
| `children` | `React.ReactNode` | - | Dropdown content |
All [Base UI Select.Popup props](https://base-ui.com/react/components/select) are forwarded via `...props`.
### SelectItem
An individual selectable option within the dropdown.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `value` | `any` | - | The value of this item |
| `disabled` | `boolean` | `false` | Whether the item is disabled |
| `className` | `string` | - | Additional CSS classes |
| `children` | `React.ReactNode` | - | Item label content |
All [Base UI Select.Item props](https://base-ui.com/react/components/select) are forwarded via `...props`.
### SelectGroup
Groups related items together with optional label.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `className` | `string` | - | Additional CSS classes |
| `children` | `React.ReactNode` | - | Group content |
All [Base UI Select.Group props](https://base-ui.com/react/components/select) are forwarded via `...props`.
### SelectLabel
Label for a group of items.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `className` | `string` | - | Additional CSS classes |
All [Base UI Select.GroupLabel props](https://base-ui.com/react/components/select) are forwarded via `...props`.
### SelectSeparator
Visual divider between items or groups.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `className` | `string` | - | Additional CSS classes |
All [Base UI Select.Separator props](https://base-ui.com/react/components/select) are forwarded via `...props`.
### SelectScrollUpButton
Scroll indicator at the top of the dropdown list.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `className` | `string` | - | Additional CSS classes |
All [Base UI Select.ScrollUpArrow props](https://base-ui.com/react/components/select) are forwarded via `...props`.
### SelectScrollDownButton
Scroll indicator at the bottom of the dropdown list.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `className` | `string` | - | Additional CSS classes |
All [Base UI Select.ScrollDownArrow props](https://base-ui.com/react/components/select) are forwarded via `...props`.
### selectTriggerVariants
A `cva` helper exported for use outside of the `` component (e.g., applying select trigger styles to custom elements).
```tsx
import { selectTriggerVariants } from "@/components/ui/select";
Custom trigger
;
```
## Accessibility
### Keyboard Interactions
| Key | Action |
| ----------- | ---------------------------------------------------------- |
| `Space` | Opens the dropdown or selects the highlighted item |
| `Enter` | Opens the dropdown or selects the highlighted item |
| `ArrowDown` | Opens the dropdown or moves highlight to the next item |
| `ArrowUp` | Opens the dropdown or moves highlight to the previous item |
| `Home` | Moves highlight to the first item |
| `End` | Moves highlight to the last item |
| `Escape` | Closes the dropdown |
| `Tab` | Closes the dropdown and moves focus to the next element |
### ARIA Attributes
- The trigger renders with `role="combobox"` and `aria-haspopup="listbox"`.
- `aria-expanded` is set to `true` when the dropdown is open.
- The popup receives `role="listbox"`.
- Each item receives `role="option"` with `aria-selected` indicating the current selection.
- `aria-disabled` is set on disabled items.
- Screen readers announce the currently selected value from the trigger.
## Compose
This component is available in [`@prototyperco/compose`](/docs/compose).
### Catalog Definition
```typescript title="select.catalog.ts"
import { z } from "zod";
import { defineComponent } from "@prototyperco/compose/catalog";
export default defineComponent({
description: "A dropdown select field for choosing from a list of options",
props: z.object({
label: z.string().optional().describe("Label displayed above the select"),
placeholder: z
.string()
.optional()
.describe("Placeholder text when no value is selected"),
options: z
.array(
z.object({
label: z.string().describe("Display text for the option"),
value: z.string().describe("Value of the option"),
}),
)
.describe("List of selectable options"),
value: z.string().optional().describe("Currently selected value"),
disabled: z.boolean().optional().describe("Whether the select is disabled"),
}),
events: ["change"],
example: {
label: "Country",
placeholder: "Select a country...",
options: [
{ label: "United States", value: "us" },
{ label: "Canada", value: "ca" },
{ label: "United Kingdom", value: "uk" },
],
},
});
```
### Example Spec
```json
{
"root": "countrySelect",
"elements": {
"countrySelect": {
"type": "Select",
"props": {
"label": "Country",
"placeholder": "Select a country...",
"options": [
{ "label": "United States", "value": "us" },
{ "label": "Canada", "value": "ca" },
{ "label": "United Kingdom", "value": "uk" }
]
},
"$bindState": { "path": "/country", "event": "change" }
}
}
}
```
Learn more in the [Compose documentation](/docs/compose).
# Separator
> a horizontal or vertical divider built on Base UI
URL: https://prototyper-ui.com/docs/components/separator
Base UI reference: https://base-ui.com/react/components/separator
```tsx
import { Separator } from "@/components/ui/separator";
export default function SeparatorDemo() {
return (
Prototyper UI
An open-source component library.
Docs
Source
Themes
);
}
```
## Installation
```bash
pnpm dlx shadcn@latest add https://prototyper-ui.com/r/separator.json
```
This will add the following files to your project:
- `components/ui/separator.tsx`
## Usage
```tsx
import { Separator } from "@/components/ui/separator";
```
```tsx
```
## Styling
### Data Slots
Use `data-slot` attributes to target the separator in CSS:
| Slot name | Element |
| ----------- | -------------------------- |
| `separator` | The separator `` root |
### Customization Examples
```css
/* Custom separator color */
[data-slot="separator"] {
@apply bg-primary/20;
}
/* Thicker horizontal separator */
[data-slot="separator"][data-horizontal] {
@apply h-0.5;
}
```
```tsx
```
## API Reference
### Separator
A visual divider between content sections.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `orientation` | `"horizontal" \| "vertical"` | `"horizontal"` | The orientation of the separator. |
| `className` | `string` | - | Additional CSS classes. |
All [Base UI Separator props](https://base-ui.com/react/components/separator) are forwarded via `...props`.
## Accessibility
### Keyboard Interactions
Separator is a non-interactive display element and does not have keyboard interactions.
### ARIA Attributes
- Renders with `role="separator"` via Base UI.
- `aria-orientation` is set to `"horizontal"` or `"vertical"` based on the `orientation` prop.
## Compose
This component is available in [`@prototyperco/compose`](/docs/compose).
### Catalog Definition
```typescript title="separator.catalog.ts"
import { z } from "zod";
import { defineComponent } from "@prototyperco/compose/catalog";
export default defineComponent({
description: "A visual divider between sections of content",
props: z.object({
orientation: z
.enum(["horizontal", "vertical"])
.optional()
.describe("Direction of the separator line, defaults to horizontal"),
decorative: z
.boolean()
.optional()
.describe(
"If true, the separator is purely decorative and hidden from assistive technology",
),
}),
events: [],
example: { orientation: "horizontal" },
});
```
### Example Spec
```json
{
"root": "sep",
"elements": {
"sep": {
"type": "Separator",
"props": {
"orientation": "horizontal"
}
}
}
}
```
Learn more in the [Compose documentation](/docs/compose).
# Skeleton
> An animated pulse placeholder for loading states
URL: https://prototyper-ui.com/docs/components/skeleton
Skeletons stand in for content while it's still loading. They preserve the layout of the real UI so the page doesn't jump when data arrives, and the soft pulse animation signals that something is happening without demanding attention. Use skeletons in place of spinners when you can predict the rough shape of the incoming content — a card, a row, an avatar — because matching the final layout reduces perceived wait time more than a generic loader.
```tsx
import { Skeleton } from "@/components/ui/skeleton";
export default function SkeletonDemo() {
return (
);
}
```
### When to use
- The shape of the data is known ahead of time (lists, cards, profile headers, table rows).
- The wait is long enough to be noticed (roughly 300ms or more) but short enough that a full-page state would be overkill.
- You want to avoid layout shift when content swaps in.
Prefer a spinner or progress indicator when the loading duration is unpredictable, when the layout cannot be approximated, or when the user has just triggered an explicit action like a form submission.
## Installation
```bash
pnpm dlx shadcn@latest add https://prototyper-ui.com/r/skeleton.json
```
This will add the following files to your project:
- `components/ui/skeleton.tsx`
## Usage
```tsx
import { Skeleton } from "@/components/ui/skeleton";
;
```
## Examples
### Card
```tsx
import { Skeleton } from "@/components/ui/skeleton";
export default function SkeletonCard() {
return (
);
}
```
## Styling
### Data Slots
Use `data-slot` attributes to target the skeleton in CSS:
| Slot name | Element |
| ---------- | ---------------- |
| `skeleton` | The `` root |
### Customization Examples
```css
/* Change animation to a shimmer */
[data-slot="skeleton"] {
@apply animate-shimmer;
}
```
```tsx
{
/* Size and shape via className */
}
;
```
## API Reference
### Skeleton
An animated loading placeholder.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `className` | `string` | - | Controls dimensions and shape. Use Tailwind utilities like h-4 w-[200px] rounded-full. |
| `children` | `React.ReactNode` | - | Optional content (typically empty). |
Extends `React.ComponentProps<"div">`. All standard div props are forwarded via `...props`.
## Accessibility
### Keyboard Interactions
Skeleton is a non-interactive presentational element and does not have keyboard interactions.
### ARIA Attributes
- Skeleton renders as a plain `
` with no implicit ARIA role.
- For screen readers, set `aria-busy="true"` on the parent container while loading and remove it when content arrives.
- If the skeleton represents content that will be announced (such as a status message), pair it with `aria-live="polite"` on the same container so assistive tech picks up the final value once it swaps in.
- Respect `prefers-reduced-motion`: the default pulse animation is subtle, but you can disable it entirely by overriding the animation utility for users who opt out.
# Slider
> A slider input for selecting values within a range
URL: https://prototyper-ui.com/docs/components/slider
Base UI reference: https://base-ui.com/react/components/slider
```tsx
import { FieldLabel } from "@/components/ui/field";
import {
Slider,
SliderControl,
SliderIndicator,
SliderOutput,
SliderThumb,
SliderTrack,
} from "@/components/ui/slider";
export default function SliderDemo() {
return (
Opacity
);
}
```
## Installation
```bash
pnpm dlx shadcn@latest add https://prototyper-ui.com/r/slider.json
```
This will add the following files to your project:
- `components/ui/slider.tsx`
## Usage
```tsx
import {
Slider,
SliderControl,
SliderTrack,
SliderIndicator,
SliderThumb,
} from "@/components/ui/slider";
;
```
## Anatomy
```tsx
```
| Sub-component | `data-slot` | Purpose | Required |
| ----------------- | ------------------ | ---------------------------------- | -------- |
| `Slider` | `slider` | Root provider, manages value state | Yes |
| `SliderControl` | `slider-control` | Touch/pointer interaction area | Yes |
| `SliderTrack` | `slider-track` | The visible track bar | Yes |
| `SliderIndicator` | `slider-indicator` | Filled portion of the track | Yes |
| `SliderThumb` | `slider-thumb` | Draggable thumb handle | Yes |
| `SliderOutput` | `slider-output` | Displays the current value | No |
## Examples
### Disabled
```tsx
import { FieldLabel } from "@/components/ui/field";
import {
Slider,
SliderControl,
SliderIndicator,
SliderThumb,
SliderTrack,
} from "@/components/ui/slider";
export default function SliderDisabled() {
return (
Cookies to share
);
}
```
### Step Values
```tsx
import { FieldLabel } from "@/components/ui/field";
import {
Slider,
SliderControl,
SliderIndicator,
SliderThumb,
SliderTrack,
} from "@/components/ui/slider";
export default function SliderStepValues() {
return (
Amount
);
}
```
### Values
```tsx
import { FieldLabel } from "@/components/ui/field";
import {
Slider,
SliderControl,
SliderIndicator,
SliderOutput,
SliderThumb,
SliderTrack,
} from "@/components/ui/slider";
export default function SliderValues() {
return (
Cookies to Buy
);
}
```
### Vertical
```tsx
import {
Slider,
SliderControl,
SliderIndicator,
SliderThumb,
SliderTrack,
} from "@/components/ui/slider";
export default function SliderVertical() {
return (
);
}
```
## Styling
### Data Slots
Use `data-slot` attributes to target specific parts of the slider:
| Slot name | Element |
| ------------------ | ------------------------------ |
| `slider` | Root wrapper |
| `slider-control` | Touch/pointer interaction area |
| `slider-track` | The visible track bar |
| `slider-indicator` | Filled portion of the track |
| `slider-thumb` | Draggable thumb handle |
| `slider-output` | Current value display |
### Customization Examples
```css
/* Custom track height */
[data-slot="slider-track"] {
@apply h-3 rounded-lg;
}
/* Custom indicator color */
[data-slot="slider-indicator"] {
@apply bg-green-600;
}
```
```tsx
{
/* Override styles via className */
}
;
```
## API Reference
### Slider
Root component that manages value state and layout.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `value` | `number \| number[]` | - | Controlled value |
| `defaultValue` | `number \| number[]` | - | Initial value for uncontrolled usage |
| `onValueChange` | `(value: number \| number[], event: Event) => void` | - | Callback when value changes |
| `onValueCommitted` | `(value: number \| number[], event: Event) => void` | - | Callback when interaction ends (e.g. pointer up) |
| `min` | `number` | `0` | Minimum value |
| `max` | `number` | `100` | Maximum value |
| `step` | `number` | `1` | Step increment |
| `largeStep` | `number` | `10` | Step increment for Page Up / Page Down |
| `orientation` | `"horizontal" \| "vertical"` | `"horizontal"` | Orientation of the slider |
| `disabled` | `boolean` | `false` | Whether the slider is disabled |
| `name` | `string` | - | Name attribute for form submission |
| `className` | `string` | - | Additional CSS classes |
All [Base UI Slider props](https://base-ui.com/react/components/slider) are forwarded via `...props`.
### SliderControl
The area that receives touch and pointer events for dragging.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `className` | `string` | - | Additional CSS classes |
All [Base UI Slider.Control props](https://base-ui.com/react/components/slider) are forwarded via `...props`.
### SliderTrack
The visible bar that represents the full range.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `className` | `string` | - | Additional CSS classes |
All [Base UI Slider.Track props](https://base-ui.com/react/components/slider) are forwarded via `...props`.
### SliderIndicator
The filled portion of the track that shows the current value.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `className` | `string` | - | Additional CSS classes |
All [Base UI Slider.Indicator props](https://base-ui.com/react/components/slider) are forwarded via `...props`.
### SliderThumb
The draggable handle that the user interacts with.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `className` | `string` | - | Additional CSS classes |
All [Base UI Slider.Thumb props](https://base-ui.com/react/components/slider) are forwarded via `...props`.
### SliderOutput
Displays the current slider value as text.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `className` | `string` | - | Additional CSS classes |
All [Base UI Slider.Value props](https://base-ui.com/react/components/slider) are forwarded via `...props`.
## Accessibility
### Keyboard Interactions
| Key | Action |
| ------------ | ------------------------------------- |
| `ArrowRight` | Increases the value by one step |
| `ArrowUp` | Increases the value by one step |
| `ArrowLeft` | Decreases the value by one step |
| `ArrowDown` | Decreases the value by one step |
| `PageUp` | Increases the value by one large step |
| `PageDown` | Decreases the value by one large step |
| `Home` | Sets the value to the minimum |
| `End` | Sets the value to the maximum |
| `Tab` | Moves focus to / away from the thumb |
### ARIA Attributes
- `Slider` renders with `role="group"` via Base UI.
- `SliderThumb` renders with `role="slider"`.
- `aria-valuenow`, `aria-valuemin`, and `aria-valuemax` are set on the thumb.
- `aria-orientation` is set based on the `orientation` prop.
- `aria-disabled` is set when the slider is disabled.
- `SliderOutput` is linked to the thumb via `aria-describedby` for screen reader announcements.
- Screen readers announce the current value and range when the thumb receives focus.
## Compose
This component is available in [`@prototyperco/compose`](/docs/compose).
### Catalog Definition
```typescript title="slider.catalog.ts"
import { z } from "zod";
import { defineComponent } from "@prototyperco/compose/catalog";
export default defineComponent({
description:
"A range slider input for selecting a numeric value within a range",
props: z.object({
label: z.string().optional().describe("Label displayed above the slider"),
min: z.number().optional().describe("Minimum value"),
max: z.number().optional().describe("Maximum value"),
step: z.number().optional().describe("Step increment"),
value: z.number().optional().describe("Current slider value (bindable)"),
disabled: z.boolean().optional().describe("Whether the slider is disabled"),
}),
events: ["change"],
example: { label: "Volume", min: 0, max: 100, step: 1, value: 50 },
});
```
### Example Spec
```json
{
"root": "volumeSlider",
"elements": {
"volumeSlider": {
"type": "Slider",
"props": {
"label": "Volume",
"min": 0,
"max": 100,
"step": 1,
"value": 50
},
"$bindState": { "path": "/volume", "event": "change" }
}
}
}
```
Learn more in the [Compose documentation](/docs/compose).
# Spinner
> An SVG loading spinner with size variants
URL: https://prototyper-ui.com/docs/components/spinner
```tsx
import { Spinner } from "@/components/ui/spinner";
export default function SpinnerDemo() {
return ;
}
```
## Installation
```bash
pnpm dlx shadcn@latest add https://prototyper-ui.com/r/spinner.json
```
This will add the following files to your project:
- `components/ui/spinner.tsx`
## Usage
```tsx
import { Spinner } from "@/components/ui/spinner";
;
```
## Examples
### Sizes
```tsx
import { Spinner } from "@/components/ui/spinner";
export default function SpinnerSizes() {
return (
);
}
```
### In Button
```tsx
import { Button } from "@/components/ui/button";
import { Spinner } from "@/components/ui/spinner";
export default function SpinnerButton() {
return (
Loading...
);
}
```
## Styling
### Data Slots
Use `data-slot` attributes to target the spinner in CSS:
| Slot name | Element |
| --------- | ---------------- |
| `spinner` | The `` root |
### Customization Examples
```css
/* Change spinner color */
[data-slot="spinner"] {
@apply text-primary;
}
```
```tsx
{
/* Custom color via className */
}
;
```
## API Reference
### Spinner
An SVG loading spinner with rotation animation.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `size` | `"sm" \| "md" \| "lg"` | `"md"` | Size of the spinner (sm=16px, md=24px, lg=32px). |
| `className` | `string` | - | Additional CSS classes. |
Extends `React.ComponentProps<"svg">`. All standard SVG props are forwarded via `...props`.
### spinnerVariants
A `cva` helper exported for use outside of the `` component.
## Accessibility
### Keyboard Interactions
Spinner is a non-interactive presentational element and does not have keyboard interactions.
### ARIA Attributes
- Add `role="status"` and `aria-label="Loading"` when the spinner indicates an ongoing operation.
- Use `aria-busy="true"` on the parent container while loading.
# Switch
> A toggle switch built on Base UI
URL: https://prototyper-ui.com/docs/components/switch
Base UI reference: https://base-ui.com/react/components/switch
```tsx
import { Switch, SwitchTrack, SwitchThumb } from "@/components/ui/switch";
export default function SwitchDemo() {
return (
Low power mode
);
}
```
## Installation
```bash
pnpm dlx shadcn@latest add https://prototyper-ui.com/r/switch.json
```
This will add the following files to your project:
- `components/ui/switch.tsx`
## Usage
```tsx
import { Switch, SwitchTrack, SwitchThumb } from "@/components/ui/switch";
Airplane Mode
;
```
## Anatomy
```tsx
{/* optional */}
{/* label text */}
```
| Sub-component | `data-slot` | Purpose | Required |
| ------------- | -------------- | -------------------------------------------- | -------- |
| `Switch` | `switch` | Root component with label and switch control | Yes |
| `SwitchTrack` | `switch-track` | The sliding track background | Yes |
| `SwitchThumb` | `switch-thumb` | The draggable thumb indicator | Yes |
| `SwitchIcon` | `switch-icon` | Optional icon wrapper inside the thumb | No |
## Examples
### Sizes
```tsx
import { Switch, SwitchTrack, SwitchThumb } from "@/components/ui/switch";
export default function SwitchSizes() {
return (
Small
Medium
Large
);
}
```
### With Icon
```tsx
import { Check } from "lucide-react";
import {
Switch,
SwitchTrack,
SwitchThumb,
SwitchIcon,
} from "@/components/ui/switch";
export default function SwitchWithIcon() {
return (
Notifications
);
}
```
### Label Left
```tsx
import { Switch, SwitchTrack, SwitchThumb } from "@/components/ui/switch";
export default function SwitchLabelLeft() {
return (
Notifications
);
}
```
### Disabled
```tsx
import { Switch, SwitchTrack, SwitchThumb } from "@/components/ui/switch";
export default function SwitchDisabled() {
return (
Airplane Mode
);
}
```
### Read Only
```tsx
import { Switch, SwitchTrack, SwitchThumb } from "@/components/ui/switch";
export default function SwitchReadonly() {
return (
Bluetooth
);
}
```
## Styling
### Data Slots
Use `data-slot` attributes to target specific parts of the switch:
| Slot name | Element |
| -------------- | ---------------------------- |
| `switch` | Root wrapper (label + track) |
| `switch-track` | The sliding track background |
| `switch-thumb` | The circular thumb indicator |
### Customization Examples
```css
/* Make the switch track wider */
[data-slot="switch-track"] {
@apply w-12 h-6;
}
/* Custom checked track color */
[data-slot="switch"] [data-slot="switch-track"] {
@apply group-data-checked:bg-green-600;
}
```
```tsx
{
/* Override styles via className */
}
Wide gap label ;
```
## API Reference
### Switch
Root component that manages on/off state and renders a track with thumb.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `checked` | `boolean` | - | Controlled checked state |
| `defaultChecked` | `boolean` | `false` | Initial checked state for uncontrolled usage |
| `onCheckedChange` | `(checked: boolean, event: Event) => void` | - | Callback when checked state changes |
| `disabled` | `boolean` | `false` | Whether the switch is disabled |
| `readOnly` | `boolean` | `false` | Whether the switch is read-only |
| `required` | `boolean` | `false` | Whether the switch is required |
| `name` | `string` | - | Name attribute for form submission |
| `className` | `string` | - | Additional CSS classes |
| `children` | `React.ReactNode` | - | Label content rendered next to the switch |
All [Base UI Switch.Root props](https://base-ui.com/react/components/switch) are forwarded via `...props`.
### SwitchTrack
The sliding track background container that holds the thumb.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `size` | `"sm" \| "md" \| "lg"` | `"md"` | Track size, defaults to size set on Switch root |
| `className` | `string` | - | Additional CSS classes |
| `children` | `React.ReactNode` | - | Content inside the track (typically SwitchThumb) |
All standard HTML `div` props are supported via `...props`.
### SwitchThumb
The circular thumb indicator that slides inside the track.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `className` | `string` | - | Additional CSS classes |
| `children` | `React.ReactNode` | - | Content inside the thumb (typically SwitchIcon) |
All [Base UI Switch.Thumb props](https://base-ui.com/react/components/switch) are forwarded via `...props`.
### SwitchIcon
Optional icon wrapper for displaying icons inside the thumb.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `className` | `string` | - | Additional CSS classes |
| `children` | `React.ReactNode` | - | Icon elements to display |
All standard HTML `span` props are supported via `...props`. The component automatically hides from screen readers with `aria-hidden="true"`.
## Accessibility
### Keyboard Interactions
| Key | Action |
| ------- | ------------------------------------- |
| `Space` | Toggles the switch on/off |
| `Tab` | Moves focus to / away from the switch |
### ARIA Attributes
- Renders with `role="switch"` via Base UI.
- `aria-checked` is set to `true` or `false` based on the switch state.
- `aria-disabled` is set when the switch is disabled.
- `aria-readonly` is set when the switch is read-only.
- Screen readers announce the label and the on/off state.
## Compose
This component is available in [`@prototyperco/compose`](/docs/compose).
### Catalog Definition
```typescript title="switch.catalog.ts"
import { z } from "zod";
import { defineComponent } from "@prototyperco/compose/catalog";
export default defineComponent({
description: "A toggle switch for on/off states",
props: z.object({
label: z.string().optional().describe("Label displayed next to the switch"),
checked: z.boolean().optional().describe("Whether the switch is on"),
disabled: z.boolean().optional().describe("Whether the switch is disabled"),
size: z.enum(["sm", "md", "lg"]).optional().describe("Switch size variant"),
}),
events: ["change"],
example: { label: "Enable notifications", checked: false, size: "md" },
});
```
### Example Spec
```json
{
"root": "notifSwitch",
"elements": {
"notifSwitch": {
"type": "Switch",
"props": {
"label": "Enable notifications",
"checked": false,
"size": "md"
},
"$bindState": { "path": "/notificationsEnabled", "event": "change" }
}
}
}
```
Learn more in the [Compose documentation](/docs/compose).
# Tabs
> Tabbed content panels built on Base UI
URL: https://prototyper-ui.com/docs/components/tabs
Base UI reference: https://base-ui.com/react/components/tabs
```tsx
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
export default function TabsDemo() {
return (
Account
Notifications
Billing
Update your name, email, and profile photo.
Choose which notifications you receive and how.
Manage your subscription plan and payment method.
);
}
```
## Installation
```bash
pnpm dlx shadcn@latest add https://prototyper-ui.com/r/tabs.json
```
This will add the following files to your project:
- `components/ui/tabs.tsx`
## Usage
```tsx
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
Tab 1
Tab 2
Content 1
Content 2
;
```
## Anatomy
```tsx
```
| Sub-component | `data-slot` | Purpose | Required |
| ------------- | -------------- | --------------------------------------- | -------- |
| `Tabs` | `tabs` | Root provider, manages active tab state | Yes |
| `TabsList` | `tabs-list` | Container for tab triggers | Yes |
| `TabsTrigger` | `tabs-trigger` | Button that activates a tab panel | Yes |
| `TabsContent` | `tabs-content` | Content panel associated with a trigger | Yes |
## Examples
### Disabled
```tsx
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
export default function TabsDisabled() {
return (
Mouse Settings
Keyboard Settings
Gamepad Settings
Mouse Settings
Keyboard Settings
Gamepad Settings
);
}
```
### Disabled Dynamic
```tsx
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
export default function TabsDisabledDynamic() {
const tabs = [
{ id: 1, title: "Mouse settings" },
{ id: 2, title: "Keyboard settings" },
{ id: 3, title: "Gamepad settings" },
];
return (
{tabs.map((item) => (
{item.title}
))}
{tabs.map((item) => (
{item.title}
))}
);
}
```
### Disabled Items
```tsx
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
export default function TabsDisabledItems() {
return (
Mouse Settings
Keyboard Settings
Gamepad Settings
Mouse Settings
Keyboard Settings
Gamepad Settings
);
}
```
### Dynamic
```tsx
"use client";
import React from "react";
import { Button } from "@/components/ui/button";
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
export default function TabsDynamic() {
const [tabs, setTabs] = React.useState([
{ id: 1, title: "Tab 1", content: "Tab body 1" },
{ id: 2, title: "Tab 2", content: "Tab body 2" },
{ id: 3, title: "Tab 3", content: "Tab body 3" },
]);
const addTab = () => {
setTabs((tabs) => [
...tabs,
{
id: tabs.length + 1,
title: `Tab ${tabs.length + 1}`,
content: `Tab body ${tabs.length + 1}`,
},
]);
};
const removeTab = () => {
if (tabs.length > 1) {
setTabs((tabs) => tabs.slice(0, -1));
}
};
return (
{tabs.map((item) => (
{item.title}
))}
Add tab
Remove tab
{tabs.map((item) => (
{item.content}
))}
);
}
```
### Focus
```tsx
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
import { Input } from "@/components/ui/text-field";
export default function TabsFocus() {
return (
Jane Doe
John Doe
Joe Bloggs
Leave a note for Jane:
Senatus Populusque Romanus.
Alea jacta est.
);
}
```
### Vertical
```tsx
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
export default function TabsVertical() {
return (
John Doe
Jane Doe
Joe Bloggs
There is no prior chat history with John Doe.
There is no prior chat history with Jane Doe.
There is no prior chat history with Joe Bloggs.
);
}
```
## Styling
### Data Slots
Use `data-slot` attributes to target specific parts of the tabs:
| Slot name | Element |
| -------------- | -------------------------- |
| `tabs` | Root wrapper |
| `tabs-list` | Container for tab triggers |
| `tabs-trigger` | Individual tab button |
| `tabs-content` | Tab panel content area |
### Customization Examples
```css
/* Make the tab list full-width */
[data-slot="tabs-list"] {
@apply w-full;
}
/* Custom active tab style */
[data-slot="tabs-trigger"][data-active] {
@apply bg-primary text-primary-foreground;
}
```
```tsx
{
/* Override list variant via className */
}
Tab 1
Tab 2
;
```
## API Reference
### Tabs
Root component that manages active tab state and context.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `defaultValue` | `any` | - | Initial active tab for uncontrolled usage |
| `value` | `any` | - | Controlled active tab value |
| `onValueChange` | `(value: any) => void` | - | Callback when the active tab changes |
| `orientation` | `"horizontal" \| "vertical"` | `"horizontal"` | Layout direction of the tabs |
| `className` | `string` | - | Additional CSS classes |
| `children` | `React.ReactNode` | - | Tabs content |
All [Base UI Tabs.Root props](https://base-ui.com/react/components/tabs) are forwarded via `...props`.
### TabsList
Container for tab triggers with variant styling.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `variant` | `"default" \| "line"` | `"default"` | Visual style variant for the tab list |
| `className` | `string` | - | Additional CSS classes |
| `children` | `React.ReactNode` | - | Tab triggers |
All [Base UI Tabs.List props](https://base-ui.com/react/components/tabs) are forwarded via `...props`.
### TabsTrigger
Button that activates a tab panel when clicked.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `value` | `any` | - | Value linking this trigger to a panel |
| `disabled` | `boolean` | `false` | Whether the trigger is disabled |
| `className` | `string` | - | Additional CSS classes |
| `children` | `React.ReactNode` | - | Tab label content |
All [Base UI Tabs.Tab props](https://base-ui.com/react/components/tabs) are forwarded via `...props`.
### TabsContent
Content panel that is shown when its associated trigger is active.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `value` | `any` | - | Value linking this panel to a trigger |
| `className` | `string` | - | Additional CSS classes |
| `children` | `React.ReactNode` | - | Panel content |
All [Base UI Tabs.Panel props](https://base-ui.com/react/components/tabs) are forwarded via `...props`.
### tabsListVariants
A `cva` helper exported for use outside of the `` component (e.g., applying tab list styles to custom elements).
```tsx
import { tabsListVariants } from "@/components/ui/tabs";
Custom tab list
;
```
## Accessibility
### Keyboard Interactions
| Key | Action |
| ------------ | ---------------------------------------------------------------- |
| `ArrowRight` | Moves focus to the next tab trigger (horizontal orientation) |
| `ArrowLeft` | Moves focus to the previous tab trigger (horizontal orientation) |
| `ArrowDown` | Moves focus to the next tab trigger (vertical orientation) |
| `ArrowUp` | Moves focus to the previous tab trigger (vertical orientation) |
| `Home` | Moves focus to the first tab trigger |
| `End` | Moves focus to the last tab trigger |
| `Space` | Activates the focused tab trigger |
| `Enter` | Activates the focused tab trigger |
| `Tab` | Moves focus into the active tab panel, then to the next element |
### ARIA Attributes
- The tab list renders with `role="tablist"`.
- Each trigger renders with `role="tab"` and `aria-selected` indicating whether it is active.
- Each content panel renders with `role="tabpanel"`.
- `aria-controls` on each trigger links to its corresponding panel.
- `aria-labelledby` on each panel links back to its corresponding trigger.
- `aria-orientation` is set on the tab list based on the `orientation` prop.
- `aria-disabled` is set on disabled tab triggers.
## Compose
This component is available in [`@prototyperco/compose`](/docs/compose).
### Catalog Definition
```typescript title="tabs.catalog.ts"
import { z } from "zod";
import { defineComponent } from "@prototyperco/compose/catalog";
export default defineComponent({
description: "A tabbed interface for switching between content panels",
props: z.object({
tabs: z
.array(
z.object({
label: z.string().describe("Tab button label"),
value: z.string().describe("Tab identifier value"),
}),
)
.describe("Tab definitions"),
value: z.string().optional().describe("Active tab value (bindable)"),
variant: z
.enum(["default", "line"])
.optional()
.describe("Visual style variant"),
}),
events: ["change"],
example: {
tabs: [
{ label: "Account", value: "account" },
{ label: "Settings", value: "settings" },
{ label: "Billing", value: "billing" },
],
value: "account",
},
});
```
### Example Spec
```json
{
"root": "tabsContainer",
"elements": {
"tabsContainer": {
"type": "Tabs",
"props": {
"tabs": [
{ "label": "Account", "value": "account" },
{ "label": "Settings", "value": "settings" },
{ "label": "Billing", "value": "billing" }
],
"value": "account"
},
"children": ["accountContent", "settingsContent", "billingContent"],
"$bindState": { "path": "/activeTab", "event": "change" }
},
"accountContent": {
"type": "Text",
"props": { "content": "Manage your account details here." }
},
"settingsContent": {
"type": "Text",
"props": { "content": "Configure your preferences." }
},
"billingContent": {
"type": "Text",
"props": { "content": "View and manage your billing." }
}
}
}
```
Learn more in the [Compose documentation](/docs/compose).
# TextField
> A text input with label and validation built on Base UI
URL: https://prototyper-ui.com/docs/components/text-field
Base UI reference: https://base-ui.com/react/components/field
```tsx
import { FieldLabel } from "@/components/ui/field";
import { Input, TextField } from "@/components/ui/text-field";
export default function TextFieldDemo() {
return (
First name
);
}
```
## Installation
```bash
pnpm dlx shadcn@latest add https://prototyper-ui.com/r/text-field.json
```
This will add the following files to your project:
- `components/ui/text-field.tsx`
> **Note:** This component depends on [Field](/docs/components/field). It will be installed automatically.
## Usage
```tsx
import { TextField, Input } from "@/components/ui/text-field";
import { FieldLabel } from "@/components/ui/field";
Name
;
```
## Anatomy
```tsx
```
Or with `TextArea`:
```tsx
```
| Sub-component | `data-slot` | Purpose | Required |
| ------------------ | ------------------- | ----------------------------------------- | -------- |
| `TextField` | `text-field` | Root field wrapper, provides form context | Yes |
| `Input` | `input` | Single-line text input | Yes\* |
| `TextArea` | `textarea` | Multi-line text input | Yes\* |
| `FieldLabel` | `field-label` | Label for the text field | No |
| `FieldDescription` | `field-description` | Descriptive helper text | No |
| `FieldError` | `field-error` | Validation error message | No |
\* Use either `Input` or `TextArea`, not both.
## Examples
### Description
```tsx
import { FieldLabel } from "@/components/ui/field";
import { Input, TextField } from "@/components/ui/text-field";
export default function TextFieldDescription() {
return (
Email
Enter an email for us to contact you about your order.
);
}
```
### Disabled
```tsx
import { FieldLabel } from "@/components/ui/field";
import { Input, TextField } from "@/components/ui/text-field";
export default function TextFieldDisabled() {
return (
Email
);
}
```
### Multiline
```tsx
import { FieldLabel } from "@/components/ui/field";
import { TextArea, TextField } from "@/components/ui/text-field";
export default function TextFieldMultiline() {
return (
Comment
);
}
```
### Read Only
```tsx
import { FieldLabel } from "@/components/ui/field";
import { Input, TextField } from "@/components/ui/text-field";
export default function TextFieldReadonly() {
return (
Email
);
}
```
### Reusable
```tsx
import { ProtoTextField } from "@/components/ui/text-field";
export default function TextfieldReusable() {
return (
);
}
```
### Validation
```tsx
import { Button } from "@/components/ui/button";
import { FieldError, FieldLabel } from "@/components/ui/field";
import { Input, TextField } from "@/components/ui/text-field";
export default function TextFieldValidation() {
return (
Email
Submit
);
}
```
## Styling
### Data Slots
Use `data-slot` attributes to target specific parts of the text field:
| Slot name | Element |
| ------------ | --------------------------- |
| `text-field` | Root wrapper (`Field.Root`) |
| `input` | The ` ` element |
| `textarea` | The `` element |
### Customization Examples
```css
/* Remove border and shadow from all inputs */
[data-slot="input"] {
@apply border-0 shadow-none;
}
/* Custom focus ring color */
[data-slot="input"]:focus {
@apply ring-2 ring-blue-500;
}
```
```tsx
{
/* Override size via className */
}
;
```
## API Reference
### TextField
Root wrapper that provides form field context. Built on Base UI `Field.Root`.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `className` | `string` | - | Additional CSS classes |
| `children` | `React.ReactNode` | - | Field content (label, input, etc.) |
All [Base UI Field props](https://base-ui.com/react/components/field) are forwarded via `...props`.
### Input
A styled single-line text input built on Base UI `Input`.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `size` | `"sm" \| "default" \| "lg"` | `"default"` | Size of the input |
| `className` | `string` | - | Additional CSS classes |
All [Base UI Input props](https://base-ui.com/react/components/input) are forwarded via `...props`.
### TextArea
A styled multi-line text area using a native `` element.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `className` | `string` | - | Additional CSS classes |
All native `textarea` props are forwarded via `...props`.
### ProtoTextField
A convenience wrapper that composes `TextField`, `Input`/`TextArea`, `FieldLabel`, `FieldDescription`, and `FieldError` into a single component.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `label` | `string` | - | Label text for the field |
| `description` | `string` | - | Helper description text |
| `errorMessage` | `string` | - | Validation error message |
| `textArea` | `boolean` | - | Render a TextArea instead of Input |
| `inputProps` | `InputPrimitive.Props & VariantProps` | - | Props passed to the Input sub-component |
| `textAreaProps` | `React.ComponentProps<"textarea">` | - | Props passed to the TextArea sub-component |
| `className` | `string` | - | Additional CSS classes |
| `children` | `React.ReactNode` | - | Custom content (replaces Input/TextArea) |
All [Base UI Field props](https://base-ui.com/react/components/field) are forwarded via `...props`.
### inputVariants
A `cva` helper exported for applying input styles outside of the ` ` component.
```tsx
import { inputVariants } from "@/components/ui/text-field";
;
```
## Accessibility
### Keyboard Interactions
| Key | Action |
| ----- | ------------------------------------ |
| `Tab` | Moves focus into or out of the input |
### ARIA Attributes
- `TextField` renders as a Base UI `Field.Root`, automatically associating the label, description, and error message with the input via `aria-labelledby`, `aria-describedby`, and `aria-errormessage`.
- When validation fails, `data-invalid` is set on the input and the field root.
- `data-disabled` is set when the field is disabled.
- Screen readers announce the label, description, and any error messages associated with the input.
# Textarea
> a multi-line text input with validation styling
URL: https://prototyper-ui.com/docs/components/textarea
```tsx
import { Textarea } from "@/components/ui/textarea";
export default function TextareaDemo() {
return ;
}
```
## Installation
```bash
pnpm dlx shadcn@latest add https://prototyper-ui.com/r/textarea.json
```
This will add the following files to your project:
- `components/ui/textarea.tsx`
## Usage
```tsx
import { Textarea } from "@/components/ui/textarea";
```
```tsx
```
## Styling
### Data Slots
Use `data-slot` attributes to target the textarea in CSS:
| Slot name | Element |
| ---------- | --------------------- |
| `textarea` | The `` root |
### Customization Examples
```css
/* Make all textareas taller */
[data-slot="textarea"] {
@apply min-h-32 text-lg;
}
```
```tsx
{
/* Override styles via className */
}
;
```
## API Reference
### Textarea
A styled multi-line text input.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `placeholder` | `string` | - | Placeholder text shown when empty. |
| `className` | `string` | - | Additional CSS classes. |
All native `textarea` props are forwarded via `...props`.
## Accessibility
### Keyboard Interactions
| Key | Action |
| ----- | --------------------------------------- |
| `Tab` | Moves focus into or out of the textarea |
### ARIA Attributes
- Renders as a native `` element.
- `aria-invalid` styling is applied when the textarea has validation errors.
- Pair with a `` or `aria-label` to ensure the textarea is accessible to screen readers.
## Compose
This component is available in [`@prototyperco/compose`](/docs/compose).
### Catalog Definition
```typescript title="textarea.catalog.ts"
import { z } from "zod";
import { defineComponent } from "@prototyperco/compose/catalog";
export default defineComponent({
description: "A multi-line text area for longer form input",
props: z.object({
label: z.string().optional().describe("Label displayed above the textarea"),
placeholder: z.string().optional().describe("Placeholder text"),
value: z.string().optional().describe("Controlled textarea value"),
rows: z.number().optional().describe("Number of visible text rows"),
disabled: z
.boolean()
.optional()
.describe("Whether the textarea is disabled"),
}),
events: ["change", "blur", "focus"],
example: { label: "Message", placeholder: "Type your message...", rows: 3 },
});
```
### Example Spec
```json
{
"root": "messageArea",
"elements": {
"messageArea": {
"type": "Textarea",
"props": {
"label": "Message",
"placeholder": "Type your message...",
"rows": 3
},
"$bindState": { "path": "/message", "event": "change" }
}
}
}
```
Learn more in the [Compose documentation](/docs/compose).
# Toast
> notification system with type variants
URL: https://prototyper-ui.com/docs/components/toast
Base UI reference: https://base-ui.com/react/components/toast
```tsx
"use client";
import { toast } from "@/components/ui/toast";
import { Button } from "@/components/ui/button";
export default function ToastDemo() {
return (
toast("Event has been created", {
description: "Sunday, December 03, 2024 at 9:00 AM",
})
}
>
Show Toast
);
}
```
## Installation
```bash
pnpm dlx shadcn@latest add https://prototyper-ui.com/r/toast.json
```
This will add the following files to your project:
- `components/ui/toast.tsx`
### Setup
Wrap your application with `ToastProvider` in your root layout:
```tsx
import { ToastProvider } from "@/components/ui/toast";
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
{children}
);
}
```
## Usage
```tsx
import { toast } from "@/components/ui/toast";
// Basic toast
toast("Event has been created");
// With description
toast("Event has been created", {
description: "Sunday, December 03, 2024 at 9:00 AM",
});
// Typed variants
toast.success("Action completed successfully");
toast.error("Something went wrong");
toast.warning("Please review before continuing");
toast.info("A new update is available");
```
## Anatomy
```tsx
{/* Your app content */}
{/* Toaster is rendered automatically inside ToastProvider */}
```
The toast system consists of three layers:
| Export | `data-slot` | Purpose | Required |
| --------------- | ----------- | ------------------------------------------------------------------ | -------- |
| `ToastProvider` | — | Root provider that wraps your app, renders `Toaster` automatically | Yes |
| `Toaster` | `toaster` | Viewport that positions and renders all active toasts | Auto |
| `toast` | — | Imperative API to create toasts from anywhere | Yes |
Each individual toast renders the following structure:
| Part | `data-slot` | Purpose |
| ------------------- | ------------------- | ----------------------------------------- |
| `Toast.Root` | `toast` | Container for a single toast notification |
| Icon | `toast-icon` | Type-specific icon (success, error, etc.) |
| Content wrapper | `toast-content` | Flex container for title and description |
| `Toast.Title` | `toast-title` | Bold title text |
| `Toast.Description` | `toast-description` | Secondary description text |
| `Toast.Action` | `toast-action` | Optional action button |
| `Toast.Close` | `toast-close` | Dismiss button |
## Examples
### Variants
```tsx
"use client";
import { toast } from "@/components/ui/toast";
import { Button } from "@/components/ui/button";
export default function ToastVariants() {
return (
toast("This is a default toast")}
>
Default
toast.success("Action completed successfully")}
>
Success
toast.error("Something went wrong")}
>
Error
toast.warning("Please review before continuing")}
>
Warning
toast.info("A new update is available")}
>
Info
);
}
```
### With Action
```tsx
"use client";
import { toast } from "@/components/ui/toast";
import { Button } from "@/components/ui/button";
export default function ToastAction() {
return (
toast("File deleted", {
description: "The file has been moved to trash.",
action: {
label: "Undo",
onClick: () => toast.success("File restored"),
},
})
}
>
Show Toast with Action
);
}
```
### Promise
```tsx
"use client";
import { toast } from "@/components/ui/toast";
import { Button } from "@/components/ui/button";
export default function ToastPromise() {
function handleClick() {
const promise = new Promise<{ name: string }>((resolve) =>
setTimeout(() => resolve({ name: "Prototyper UI" }), 2000),
);
toast.promise(promise, {
loading: { title: "Loading..." },
success: (data) => ({ title: `${data.name} has been added` }),
error: { title: "Something went wrong" },
});
}
return (
Show Promise Toast
);
}
```
## Styling
### Data Slots
Use `data-slot` attributes to target specific parts of the toast:
| Slot name | Element |
| ------------------- | ------------------------------------- |
| `toaster` | The viewport container for all toasts |
| `toast` | Individual toast root |
| `toast-icon` | Type-specific icon |
| `toast-content` | Content wrapper (title + description) |
| `toast-title` | Title text |
| `toast-description` | Description text |
| `toast-action` | Action button |
| `toast-close` | Dismiss button |
### Customization Examples
```css
/* Change toast position to top-center */
[data-slot="toaster"] {
@apply bottom-auto top-4 right-auto left-1/2 -translate-x-1/2;
}
/* Wider toasts */
[data-slot="toaster"] {
@apply w-[420px];
}
/* Custom success icon color */
[data-slot="toast-icon"] {
@apply text-foreground;
}
```
```tsx
{
/* Custom duration */
}
toast("Quick notification", { duration: 2000 });
{
/* With action button */
}
toast("Item deleted", {
action: {
label: "Undo",
onClick: () => console.log("Undo clicked"),
},
});
```
## API Reference
### ToastProvider
Root provider that wraps your application. Renders the `Toaster` viewport automatically.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `children` | `React.ReactNode` | - | Your application content |
### toast(title, options?)
Imperative function to create a toast notification. Can be called from anywhere in your app.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `title` | `string` | - | The toast title text |
### toast.success / toast.error / toast.warning / toast.info
Typed variants that display a corresponding icon. Accept the same `(title, options?)` signature as `toast()`.
| Method | Icon | Color |
| --------------- | ------------------- | ----------- |
| `toast.success` | `CircleCheckIcon` | Emerald 500 |
| `toast.error` | `OctagonXIcon` | Red 500 |
| `toast.warning` | `TriangleAlertIcon` | Amber 500 |
| `toast.info` | `InfoIcon` | Blue 500 |
### toast.promise(promise, options)
Tracks a promise and shows loading, success, and error states.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `promise` | `Promise` | - | The promise to track |
### Toaster
The viewport component that renders all active toasts. Rendered automatically by `ToastProvider`.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `className` | `string` | - | CSS class name for the viewport container |
All [Base UI Toast Viewport props](https://base-ui.com/react/components/toast) are forwarded via `...props`.
## Accessibility
### Keyboard Interactions
| Key | Action |
| -------- | --------------------------- |
| `Escape` | Dismisses all active toasts |
### ARIA Attributes
- Each toast is rendered as an ARIA live region via Base UI's `Toast.Root`.
- `Toast.Title` is used as the accessible label for the toast.
- `Toast.Description` provides additional context.
- `Toast.Close` includes `aria-label="Dismiss"` for screen readers.
- Toasts support swipe-to-dismiss gestures on touch devices.
- Motion is reduced when the user has `prefers-reduced-motion` enabled.
### Screen Reader Behavior
- New toasts are announced automatically via ARIA live regions.
- The `Toast.Action` button is focusable and announced with its label.
- The close button announces "Dismiss" to assistive technology.
# Toggle
> A two-state toggle button built on Base UI
URL: https://prototyper-ui.com/docs/components/toggle
Base UI reference: https://base-ui.com/react/components/toggle
```tsx
import { Bold } from "lucide-react";
import { Toggle } from "@/components/ui/toggle";
export default function ToggleDemo() {
return (
);
}
```
## Installation
```bash
pnpm dlx shadcn@latest add https://prototyper-ui.com/r/toggle.json
```
This will add the following files to your project:
- `components/ui/toggle.tsx`
## Usage
```tsx
import { Toggle } from "@/components/ui/toggle";
Bold ;
```
## Examples
### Outline
```tsx
import { Italic } from "lucide-react";
import { Toggle } from "@/components/ui/toggle";
export default function ToggleOutline() {
return (
);
}
```
### Small
```tsx
import { Italic } from "lucide-react";
import { Toggle } from "@/components/ui/toggle";
export default function ToggleSm() {
return (
);
}
```
### Large
```tsx
import { Italic } from "lucide-react";
import { Toggle } from "@/components/ui/toggle";
export default function ToggleLg() {
return (
);
}
```
### Disabled
```tsx
import { Underline } from "lucide-react";
import { Toggle } from "@/components/ui/toggle";
export default function ToggleDisabled() {
return (
);
}
```
### With Text
```tsx
import { Italic } from "lucide-react";
import { Toggle } from "@/components/ui/toggle";
export default function ToggleWithText() {
return (
Italic
);
}
```
## Styling
### Data Slots
Use `data-slot` attributes to target the toggle in CSS:
| Slot name | Element |
| --------- | ------------------- |
| `toggle` | The `` root |
### Customization Examples
```css
/* Style all pressed toggles */
[data-slot="toggle"][aria-pressed="true"] {
@apply bg-primary text-primary-foreground;
}
```
```tsx
{
/* Use className for one-off overrides */
}
Pill Toggle ;
```
## API Reference
### Toggle
A two-state toggle button that can be pressed or unpressed.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `variant` | `"default" \| "outline"` | `"default"` | Visual style variant |
| `size` | `"default" \| "sm" \| "lg"` | `"default"` | Size of the toggle |
| `pressed` | `boolean` | - | Controlled pressed state |
| `onPressedChange` | `(pressed: boolean) => void` | - | Callback when pressed state changes |
| `defaultPressed` | `boolean` | `false` | Initial pressed state for uncontrolled usage |
| `disabled` | `boolean` | `false` | Whether the toggle is disabled |
| `className` | `string` | - | Additional CSS classes |
| `children` | `React.ReactNode` | - | Toggle content |
All [Base UI Toggle props](https://base-ui.com/react/components/toggle) are forwarded via `...props`.
### toggleVariants
A `cva` helper exported for use outside of the `` component.
```tsx
import { toggleVariants } from "@/components/ui/toggle";
Custom Toggle
;
```
## Accessibility
### Keyboard Interactions
| Key | Action |
| ------- | ------------------------------------- |
| `Space` | Toggles the pressed state |
| `Enter` | Toggles the pressed state |
| `Tab` | Moves focus to / away from the toggle |
### ARIA Attributes
- Renders as a `` element with `aria-pressed` attribute.
- `aria-pressed="true"` when the toggle is in the pressed state, `aria-pressed="false"` when unpressed.
- Screen readers announce the toggle as a toggle button with its current pressed/unpressed state.
- `disabled` attribute is set when the toggle is disabled.
# Toggle Group
> group of toggles with single or multiple selection
URL: https://prototyper-ui.com/docs/components/toggle-group
Base UI reference: https://base-ui.com/react/components/toggle-group
```tsx
import { AlignCenter, AlignLeft, AlignRight } from "lucide-react";
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
export default function ToggleGroupDemo() {
return (
);
}
```
## Installation
```bash
pnpm dlx shadcn@latest add https://prototyper-ui.com/r/toggle-group.json
```
This will add the following files to your project:
- `components/ui/toggle-group.tsx`
## Usage
```tsx
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
Left
Center
Right
;
```
## Anatomy
```tsx
```
| Sub-component | `data-slot` | Purpose | Required |
| ----------------- | ------------------- | ----------------------------------------- | -------- |
| `ToggleGroup` | `toggle-group` | Root container, manages selection state | Yes |
| `ToggleGroupItem` | `toggle-group-item` | Individual toggle button within the group | Yes |
## Examples
### Outline
```tsx
import { AlignCenter, AlignLeft, AlignRight } from "lucide-react";
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
export default function ToggleGroupOutline() {
return (
);
}
```
### Multiple
```tsx
import { Bold, Italic, Strikethrough, Underline } from "lucide-react";
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
export default function ToggleGroupMultiple() {
return (
);
}
```
### Vertical
```tsx
import { AlignCenter, AlignJustify, AlignLeft, AlignRight } from "lucide-react";
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
export default function ToggleGroupVertical() {
return (
);
}
```
### Small
```tsx
import { AlignCenter, AlignLeft, AlignRight } from "lucide-react";
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
export default function ToggleGroupSm() {
return (
);
}
```
### Large
```tsx
import { AlignCenter, AlignLeft, AlignRight } from "lucide-react";
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
export default function ToggleGroupLg() {
return (
);
}
```
### Disabled
```tsx
import { AlignCenter, AlignLeft, AlignRight } from "lucide-react";
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
export default function ToggleGroupDisabled() {
return (
);
}
```
## Styling
### Data Slots
Use `data-slot` attributes to target specific parts of the toggle group:
| Slot name | Element |
| ------------------- | ----------------------------------- |
| `toggle-group` | Root container for all toggle items |
| `toggle-group-item` | Individual toggle button |
### Customization Examples
```css
/* Style all pressed items in a toggle group */
[data-slot="toggle-group-item"][data-state="on"] {
@apply bg-primary text-primary-foreground;
}
/* Add spacing between items */
[data-slot="toggle-group"] {
@apply gap-1;
}
```
```tsx
{
/* Override styles via className */
}
A
B
;
```
## API Reference
### ToggleGroup
Root container that manages which toggle items are pressed.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `value` | `string[]` | - | Controlled pressed values |
| `defaultValue` | `string[]` | - | Initial pressed values for uncontrolled usage |
| `onValueChange` | `(value: string[]) => void` | - | Callback when pressed values change |
| `multiple` | `boolean` | `false` | Whether multiple items can be pressed simultaneously |
| `disabled` | `boolean` | `false` | Whether all toggle items are disabled |
| `orientation` | `"horizontal" \| "vertical"` | `"horizontal"` | Layout direction of the toggle group |
| `variant` | `"default" \| "outline"` | `"default"` | Visual style variant applied to all items |
| `size` | `"default" \| "sm" \| "lg"` | `"default"` | Size applied to all items |
| `spacing` | `number` | `0` | Gap between items (0 creates a joined look) |
| `className` | `string` | - | Additional CSS classes |
| `children` | `React.ReactNode` | - | Toggle group content |
All [Base UI ToggleGroup props](https://base-ui.com/react/components/toggle-group) are forwarded via `...props`.
### ToggleGroupItem
An individual toggle button within the group.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `value` | `string` | - | Unique value for this toggle item (required) |
| `variant` | `"default" \| "outline"` | `"default"` | Visual style variant (overridden by group variant) |
| `size` | `"default" \| "sm" \| "lg"` | `"default"` | Size of the toggle item (overridden by group size) |
| `disabled` | `boolean` | `false` | Whether this individual item is disabled |
| `className` | `string` | - | Additional CSS classes |
| `children` | `React.ReactNode` | - | Toggle item content |
All [Base UI Toggle props](https://base-ui.com/react/components/toggle) are forwarded via `...props`.
## Accessibility
### Keyboard Interactions
| Key | Action |
| ------------ | --------------------------------------------------------- |
| `Space` | Toggles the pressed state of the focused item |
| `Enter` | Toggles the pressed state of the focused item |
| `Tab` | Moves focus into / out of the toggle group |
| `ArrowRight` | Moves focus to the next item (horizontal orientation) |
| `ArrowLeft` | Moves focus to the previous item (horizontal orientation) |
| `ArrowDown` | Moves focus to the next item (vertical orientation) |
| `ArrowUp` | Moves focus to the previous item (vertical orientation) |
| `Home` | Moves focus to the first item |
| `End` | Moves focus to the last item |
### ARIA Attributes
- `ToggleGroup` renders as a `group` with `aria-label` describing the group purpose.
- Each `ToggleGroupItem` renders as a `` with `aria-pressed` attribute.
- `aria-pressed="true"` when the item is in the pressed state, `aria-pressed="false"` when unpressed.
- `aria-disabled` is set when the group or an individual item is disabled.
- `aria-orientation` is set to match the `orientation` prop.
- Focus management follows the roving tabindex pattern so only one item is tabbable at a time.
# Toolbar
> A toolbar with keyboard navigation built on Base UI
URL: https://prototyper-ui.com/docs/components/toolbar
Base UI reference: https://base-ui.com/react/components/toolbar
```tsx
"use client";
import { Bold, Italic, Underline } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
Checkbox,
CheckboxControl,
CheckboxIndicator,
} from "@/components/ui/checkbox";
import { Toggle } from "@/components/ui/toggle";
import { Toolbar, ToolbarSeparator } from "@/components/ui/toolbar";
export default function ToolbarDemo() {
return (
Copy
Paste
Cut
Night Mode
);
}
```
## Installation
```bash
pnpm dlx shadcn@latest add https://prototyper-ui.com/r/toolbar.json
```
This will add the following files to your project:
- `components/ui/toolbar.tsx`
## Usage
```tsx
import {
Toolbar,
ToolbarButton,
ToolbarSeparator,
} from "@/components/ui/toolbar";
Bold
Italic
Link
;
```
## Anatomy
```tsx
```
| Sub-component | `data-slot` | Purpose | Required |
| ------------------ | ------------------- | --------------------------------------- | -------- |
| `Toolbar` | `toolbar` | Root container with keyboard navigation | Yes |
| `ToolbarButton` | `toolbar-button` | A button within the toolbar | No |
| `ToolbarLink` | `toolbar-link` | A link within the toolbar | No |
| `ToolbarGroup` | `toolbar-group` | Groups related toolbar items together | No |
| `ToolbarSeparator` | `toolbar-separator` | Visual separator between toolbar items | No |
| `ToolbarInput` | `toolbar-input` | A text input within the toolbar | No |
## Examples
### Vertical
```tsx
"use client";
import { MousePointer, Move, PenLine, Pencil, Wand2 } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Toolbar, ToolbarSeparator } from "@/components/ui/toolbar";
export default function ToolbarVerticalDemo() {
return (
);
}
```
## Styling
### Data Slots
Use `data-slot` attributes to target specific parts of the toolbar:
| Slot name | Element |
| ------------------- | --------------------------------- |
| `toolbar` | Root toolbar container |
| `toolbar-button` | Button element within the toolbar |
| `toolbar-link` | Link element within the toolbar |
| `toolbar-group` | Group wrapper for related items |
| `toolbar-separator` | Separator line between items |
| `toolbar-input` | Text input within the toolbar |
### Customization Examples
```css
/* Add a border around the toolbar */
[data-slot="toolbar"] {
@apply rounded-lg border bg-background p-1;
}
/* Style toolbar buttons with more padding */
[data-slot="toolbar-button"] {
@apply px-4;
}
```
```tsx
{
/* Override styles via className */
}
Bold
;
```
## API Reference
### Toolbar
Root container that provides keyboard navigation between toolbar items.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `orientation` | `"horizontal" \| "vertical"` | `"horizontal"` | Layout direction of the toolbar |
| `disabled` | `boolean` | `false` | Disables all items in the toolbar |
| `loop` | `boolean` | `true` | Whether keyboard navigation loops around |
| `className` | `string` | - | Additional CSS classes |
| `children` | `React.ReactNode` | - | Toolbar content |
All [Base UI Toolbar.Root props](https://base-ui.com/react/components/toolbar) are forwarded via `...props`.
### ToolbarButton
A button within the toolbar that participates in keyboard navigation.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `disabled` | `boolean` | `false` | Whether the button is disabled |
| `className` | `string` | - | Additional CSS classes |
| `children` | `React.ReactNode` | - | Button content |
All [Base UI Toolbar.Button props](https://base-ui.com/react/components/toolbar) are forwarded via `...props`.
### ToolbarLink
A link within the toolbar that participates in keyboard navigation.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `href` | `string` | - | The URL the link points to |
| `className` | `string` | - | Additional CSS classes |
| `children` | `React.ReactNode` | - | Link content |
All [Base UI Toolbar.Link props](https://base-ui.com/react/components/toolbar) are forwarded via `...props`.
### ToolbarGroup
Groups related toolbar items together visually and semantically.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `disabled` | `boolean` | `false` | Disables all items in the group |
| `className` | `string` | - | Additional CSS classes |
| `children` | `React.ReactNode` | - | Group content |
All [Base UI Toolbar.Group props](https://base-ui.com/react/components/toolbar) are forwarded via `...props`.
### ToolbarSeparator
A visual separator between toolbar items or groups.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `orientation` | `"horizontal" \| "vertical"` | `"vertical"` | Direction of the separator line |
| `className` | `string` | - | Additional CSS classes |
All [Base UI Toolbar.Separator props](https://base-ui.com/react/components/toolbar) are forwarded via `...props`.
### ToolbarInput
A text input within the toolbar that participates in keyboard navigation.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `className` | `string` | - | Additional CSS classes |
All [Base UI Toolbar.Input props](https://base-ui.com/react/components/toolbar) are forwarded via `...props`.
## Accessibility
### Keyboard Interactions
| Key | Action |
| ----------------- | ----------------------------------------------------------------- |
| `Tab` | Moves focus into the toolbar (focuses the first/last active item) |
| `ArrowRight` | Moves focus to the next toolbar item (horizontal orientation) |
| `ArrowLeft` | Moves focus to the previous toolbar item (horizontal orientation) |
| `ArrowDown` | Moves focus to the next toolbar item (vertical orientation) |
| `ArrowUp` | Moves focus to the previous toolbar item (vertical orientation) |
| `Home` | Moves focus to the first toolbar item |
| `End` | Moves focus to the last toolbar item |
| `Space` / `Enter` | Activates the focused toolbar button or link |
### ARIA Attributes
- `Toolbar` renders with `role="toolbar"` by default via Base UI.
- `aria-orientation` is set to match the `orientation` prop.
- `aria-disabled` is set on the toolbar when `disabled` is `true`.
- Toolbar items are part of a single tab stop; arrow keys navigate between items within the toolbar.
- Focus management follows the roving tabindex pattern so only one item is tabbable at a time.
# Tooltip
> a tooltip that appears on hover built on Base UI
URL: https://prototyper-ui.com/docs/components/tooltip
Base UI reference: https://base-ui.com/react/components/tooltip
```tsx
import { PencilIcon } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
export default function TooltipDemo() {
return (
}
>
Edit
);
}
```
## Installation
```bash
pnpm dlx shadcn@latest add https://prototyper-ui.com/r/tooltip.json
```
This will add the following files to your project:
- `components/ui/tooltip.tsx`
## Usage
```tsx
import {
Tooltip,
TooltipTrigger,
TooltipContent,
TooltipProvider,
} from "@/components/ui/tooltip";
Hover me
Tooltip text
;
```
## Anatomy
```tsx
```
| Sub-component | `data-slot` | Purpose | Required |
| ----------------- | ------------------ | ------------------------------------------- | -------- |
| `TooltipProvider` | `tooltip-provider` | Shared delay and configuration for tooltips | Yes |
| `Tooltip` | `tooltip` | Root provider, manages open/close state | Yes |
| `TooltipTrigger` | `tooltip-trigger` | Element that triggers the tooltip on hover | Yes |
| `TooltipContent` | `tooltip-content` | The popup displaying tooltip text | Yes |
## Examples
### Cross Offset
```tsx
import { ArrowRightIcon } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
export default function TooltipOffset() {
return (
}
>
This will shift over to the right.
);
}
```
### Disabled
```tsx
import { PencilIcon } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
export default function TooltipDisabled() {
return (
}
>
Edit
);
}
```
### Offset
```tsx
import { ArrowUpIcon } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
export default function TooltipOffset() {
return (
}
>
This will shift up.
);
}
```
### Position
```tsx
import { Button } from "@/components/ui/button";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
export default function TooltipPosition() {
return (
}>
Left
Add to library
}>
Up
Add to library
}>
Down
Add to library
}>
Right
Add to library
);
}
```
## Styling
### Data Slots
Use `data-slot` attributes to target specific parts of the tooltip:
| Slot name | Element |
| ------------------ | --------------------------------- |
| `tooltip-provider` | Shared provider (no DOM rendered) |
| `tooltip` | Root provider (no DOM rendered) |
| `tooltip-trigger` | The trigger element |
| `tooltip-content` | The popup panel |
| `tooltip-arrow` | Arrow element pointing to trigger |
### Customization Examples
```css
/* Change tooltip background */
[data-slot="tooltip-content"] {
@apply bg-primary text-primary-foreground;
}
/* Style the tooltip arrow */
[data-slot="tooltip-arrow"] {
@apply bg-primary fill-primary;
}
```
```tsx
{
/* Override styles via className */
}
This action is destructive
;
```
## API Reference
### TooltipProvider
Shared provider that configures delay and grouping behavior for all child tooltips.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `delay` | `number` | `0` | Delay in ms before tooltips appear |
All [Base UI Tooltip.Provider props](https://base-ui.com/react/components/tooltip) are forwarded via `...props`.
### Tooltip
Root component that manages open/close state for a single tooltip.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `open` | `boolean` | - | Controlled open state |
| `onOpenChange` | `(open: boolean) => void` | - | Callback when open state changes |
| `defaultOpen` | `boolean` | `false` | Initial open state for uncontrolled usage |
All [Base UI Tooltip.Root props](https://base-ui.com/react/components/tooltip) are forwarded via `...props`.
### TooltipTrigger
Element that triggers the tooltip on hover and focus.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `className` | `string` | - | Additional CSS classes |
All [Base UI Tooltip.Trigger props](https://base-ui.com/react/components/tooltip) are forwarded via `...props`.
### TooltipContent
The popup panel displaying the tooltip text, rendered inside a portal with a positioner.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `side` | `"top" \| "bottom" \| "left" \| "right"` | `"top"` | Preferred side relative to trigger |
| `sideOffset` | `number` | `8` | Gap between trigger and popup |
| `align` | `"start" \| "center" \| "end"` | `"center"` | Alignment relative to the trigger |
| `alignOffset` | `number` | `0` | Offset from the alignment edge |
| `className` | `string` | - | Additional CSS classes |
| `children` | `React.ReactNode` | - | Tooltip content |
All [Base UI Tooltip.Popup props](https://base-ui.com/react/components/tooltip) are forwarded via `...props`.
## Accessibility
### Keyboard Interactions
| Key | Action |
| -------- | ----------------------------------------------- |
| `Tab` | Moves focus to the trigger, showing the tooltip |
| `Escape` | Closes the tooltip |
### ARIA Attributes
- `TooltipContent` receives `role="tooltip"` via Base UI.
- The trigger element receives `aria-describedby` pointing to the tooltip content.
- Tooltips appear on both hover and focus, ensuring keyboard accessibility.
- The tooltip includes an arrow element for visual connection to the trigger.
- Screen readers announce tooltip content when the trigger receives focus.
## Compose
This component is available in [`@prototyperco/compose`](/docs/compose).
### Catalog Definition
```typescript title="tooltip.catalog.ts"
import { z } from "zod";
import { defineComponent } from "@prototyperco/compose/catalog";
export default defineComponent({
description:
"A tooltip that displays informational text when hovering over its trigger content",
props: z.object({
content: z.string().describe("Tooltip text to display on hover"),
}),
events: [],
example: { content: "More information" },
});
```
### Example Spec
```json
{
"root": "tip",
"elements": {
"tip": {
"type": "Tooltip",
"props": {
"content": "More information"
},
"children": ["triggerBtn"]
},
"triggerBtn": {
"type": "Button",
"props": { "label": "Hover me", "variant": "outline" }
}
}
}
```
Learn more in the [Compose documentation](/docs/compose).
# Tree View
> A hierarchical tree view with collapsible nodes and keyboard navigation built on Base UI
URL: https://prototyper-ui.com/docs/components/tree-view
Base UI reference: https://base-ui.com/react/components/collapsible
```tsx
import {
TreeView,
TreeViewItem,
TreeViewGroup,
TreeViewLeaf,
} from "@/components/ui/tree-view";
export default function TreeViewDemo() {
return (
button.tsx
dialog.tsx
input.tsx
utils.ts
app.tsx
index.tsx
favicon.ico
index.html
package.json
tsconfig.json
);
}
```
## Installation
```bash
pnpm dlx shadcn@latest add https://prototyper-ui.com/r/tree-view.json
```
This will add the following files to your project:
- `components/ui/tree-view.tsx`
## Usage
```tsx
import {
TreeView,
TreeViewItem,
TreeViewGroup,
TreeViewLeaf,
} from "@/components/ui/tree-view";
index.tsx
app.tsx
package.json
;
```
## Anatomy
```tsx
```
| Sub-component | `data-slot` | Purpose | Required |
| --------------- | ----------------- | ------------------------------------------- | -------- |
| `TreeView` | `tree-view` | Root container with keyboard navigation | Yes |
| `TreeViewItem` | `tree-view-item` | Collapsible node with chevron and label | Yes |
| `TreeViewGroup` | `tree-view-group` | Animated collapsible container for children | Yes |
| `TreeViewLeaf` | `tree-view-leaf` | Terminal node (no children) | No |
## Examples
### Default expanded
```tsx
getting-started.md
api-reference.md
```
### Controlled expanded keys
```tsx
const [expandedKeys, setExpandedKeys] = useState(new Set(["src"]))
index.tsx
```
## Styling
### Data Slots
| Slot name | Element |
| ---------------------- | --------------------------------------- |
| `tree-view` | Root `div` with `role="tree"` |
| `tree-view-item` | Collapsible node row with chevron |
| `tree-view-item-icon` | Chevron icon (rotates when expanded) |
| `tree-view-item-label` | Label text |
| `tree-view-group` | Animated collapsible panel for children |
| `tree-view-leaf` | Terminal item row |
### Customization Examples
```css
/* Custom item hover color */
[data-slot="tree-view-item"]:hover,
[data-slot="tree-view-leaf"]:hover {
@apply bg-primary/10;
}
/* Custom chevron color */
[data-slot="tree-view-item-icon"] {
@apply text-primary;
}
```
## API Reference
### TreeView
Root container that provides context, keyboard navigation, and expanded state management.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `expandedKeys` | `Set` | - | Controlled set of expanded item keys |
| `onExpandedKeysChange` | `(keys: Set) => void` | - | Callback when expanded keys change |
| `defaultExpandedKeys` | `Set` | - | Default expanded keys (uncontrolled) |
| `defaultExpandAll` | `boolean` | `false` | Expand all nodes by default |
| `className` | `string` | - | Additional CSS classes |
All standard `div` props are forwarded.
### TreeViewItem
Collapsible node with a chevron icon and label. Uses Base UI `Collapsible.Root` internally.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `itemKey` | `string` | - | Unique key for this item (used for expand/collapse tracking) |
| `label` | `React.ReactNode` | - | Label content displayed next to the chevron |
| `depth` | `number` | - | Depth level (auto-managed, can override) |
| `className` | `string` | - | Additional CSS classes |
All standard `div` props are forwarded.
### TreeViewGroup
Animated collapsible container for child items. Uses Base UI `Collapsible.Panel`.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `className` | `string` | - | Additional CSS classes |
All standard `div` props are forwarded.
### TreeViewLeaf
Terminal node (no children). Renders with extra left padding to align with item labels.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `depth` | `number` | - | Depth level (auto-managed, can override) |
| `className` | `string` | - | Additional CSS classes |
All standard `div` props are forwarded.
## Accessibility
### Keyboard Interactions
| Key | Action |
| --------------- | ------------------------------------------------------------- |
| `ArrowDown` | Moves focus to the next visible item |
| `ArrowUp` | Moves focus to the previous visible item |
| `ArrowRight` | Expands a collapsed item, or moves to first child if expanded |
| `ArrowLeft` | Collapses an expanded item, or moves to parent |
| `Home` | Moves focus to the first visible item |
| `End` | Moves focus to the last visible item |
| `Enter`/`Space` | Toggles expand/collapse on a tree item |
### ARIA Attributes
- The root element has `role="tree"`.
- Each `TreeViewItem` and `TreeViewLeaf` has `role="treeitem"`.
- `TreeViewItem` has `aria-expanded` reflecting its expand state.
- `TreeViewGroup` has `role="group"`.
- Depth-based indentation provides visual hierarchy.
# Compose
> AI-generated interfaces powered by Prototyper UI
URL: https://prototyper-ui.com/docs/compose
Compose lets AI models generate live, interactive user interfaces using Prototyper UI components. Models output streaming JSON patches, and Compose progressively renders them into real React components.
## How It Works
1. An AI model receives a system prompt describing available UI components
2. The model outputs JSONL (one JSON Patch operation per line)
3. Compose applies patches progressively, building a component tree
4. The rendered UI is fully interactive — forms bind to state, buttons trigger actions
## Quick Example
```tsx
import { Renderer } from "@prototyperco/compose"
import { prototyperComponents } from "@prototyperco/compose/components"
const spec = {
root: "card",
elements: {
card: { type: "Card", props: {}, children: ["heading", "btn"] },
heading: { type: "Heading", props: { text: "Hello World", level: 2 } },
btn: {
type: "Button",
props: { label: "Click me" },
on: { press: { action: "setState", params: { path: "/clicked", value: true } } },
},
},
state: { clicked: false },
}
```
## Key Features
- **20 components** — Button, Card, Input, Tabs, Dialog, and more
- **Streaming** — UI builds progressively as the model generates
- **State management** — Two-way data binding with `$bindState`
- **Actions** — Built-in setState, pushState, removeState, toggleState, navigate
- **Dynamic expressions** — `$state`, `$cond`, `$template`, `$item` for data binding
- **Visibility** — Conditional rendering based on state
- **Validation** — 14 built-in field validators
- **DX helpers** — Builder functions for actions, visibility, and validation with full autocomplete
- **Type safety** — Infer spec types from catalog definitions with zero manual typing
- **Store adapters** — Sync Compose state with Zustand, Redux, or Jotai
- **Prompt engineering** — Modes, templates, and structured output for OpenAI and Anthropic
- **Code export** — Convert any spec to copy-paste JSX with `specToJSX()`
## Architecture
Compose is a standalone package (`@prototyperco/compose`) with zero AI SDK dependencies:
- **Core engine** — Framework-agnostic TypeScript: types, state, patching, expressions
- **React renderer** — Providers, hooks, and component wrappers
- **Catalog system** — Zod-based component definitions with prompt generation
- **Codegen** — Spec to JSX code export
```
@prototyperco/compose
├── /core — Types, state store, JSON Patch, expressions, visibility, validation
├── /react — Renderer, ComposeProvider, hooks (useUIStream, useBoundProp, etc.)
├── /components — 20 component wrappers + prototyperComponents registry
├── /catalog — Component catalog builder + LLM prompt generation
└── /codegen — specToJSX() converter
```
## Live Canvas
Want real-time AI-powered design? The [Live Canvas](/design) connects AI agents (via MCP) directly to the browser. Changes to specs and themes are pushed over WebSocket and rendered instantly — no manual copy-paste needed. See the [MCP Integration](/docs/compose/mcp) page for setup details.
## Next Steps
- [Getting Started](/docs/compose/getting-started) — Install and render your first spec
- [Spec Format](/docs/compose/spec-format) — Understand the flat spec structure
- [Streaming](/docs/compose/streaming) — Connect to an AI model
- [Expressions](/docs/compose/expressions) — Dynamic values and data binding
- [Actions](/docs/compose/actions) — Built-in actions and event handling
- [Visibility](/docs/compose/visibility) — Conditional rendering based on state
- [Validation](/docs/compose/validation) — Field validation rules and error messages
- [DX Helpers](/docs/compose/helpers) — Convenience builders for actions, checks, and visibility
- [Type Safety](/docs/compose/type-safety) — TypeScript inference and type-safe spec building
- [Store Adapters](/docs/compose/store-adapters) — Integrate with Zustand, Redux, or Jotai
- [Prompt Engineering](/docs/compose/prompts) — Customize AI prompts and structured outputs
- [Catalog](/docs/compose/catalog) — Define and register component catalogs
- [Code Export](/docs/compose/code-export) — Convert specs to copy-paste JSX
- [API Reference](/docs/compose/api-reference) — Complete API documentation
# Actions & Events
> Handle user interactions with built-in and custom action handlers
URL: https://prototyper-ui.com/docs/compose/actions
Actions connect user interactions to state changes and side effects. When a user clicks a button, types in an input, or interacts with any component, actions define what happens.
## The `on` Field
Every element can define event handlers via the `on` field. Keys are event names, values are `ActionBinding` objects:
```json
{
"type": "Button",
"props": { "label": "Save" },
"on": {
"press": {
"action": "setState",
"params": { "path": "/saved", "value": true }
}
}
}
```
## ActionBinding Type
```ts
interface ActionBinding {
/** Name of the action handler to invoke. */
action: string;
/** Parameters passed to the handler (may contain expressions). */
params?: Record;
/** Optional confirmation dialog shown before execution. */
confirm?: ActionConfirm;
/** Follow-up action to run on success. */
onSuccess?: ActionOnSuccess;
/** Follow-up action to run on error. */
onError?: ActionOnError;
/** If true, call event.preventDefault() before executing the action. */
preventDefault?: boolean;
}
type ActionOnSuccess =
| { navigate: string }
| { set: Record }
| { action: string; params?: Record };
type ActionOnError =
| { set: Record }
| { action: string; params?: Record };
```
## Events by Component
Different components emit different events:
| Component | Events | Description |
| -------------- | ---------------- | ----------------------------------- |
| **Button** | `press` | User clicks the button |
| **Input** | `change`, `blur` | Value changes, input loses focus |
| **Textarea** | `change`, `blur` | Value changes, textarea loses focus |
| **Select** | `change` | Selection changes |
| **Checkbox** | `change` | Checked state changes |
| **Switch** | `change` | Toggled on/off |
| **RadioGroup** | `change` | Selected option changes |
| **Slider** | `change` | Value changes |
| **Tabs** | `change` | Active tab changes |
| **Accordion** | `change` | Expanded item changes |
| **Dialog** | `close` | Dialog is dismissed |
## Built-in Actions
Compose includes 5 built-in action handlers that cover common state mutations. No custom code is needed for these.
### `setState`
Set a value at a state path:
```json
{
"action": "setState",
"params": { "path": "/form/submitted", "value": true }
}
```
### `pushState`
Append a value to an array at a state path:
```json
{
"action": "pushState",
"params": {
"path": "/todos",
"value": { "id": "3", "title": "New todo", "done": false }
}
}
```
If the value at `path` is not an array, a new array is created with the value as its only element.
### `removeState`
Remove an element from an array by index:
```json
{
"action": "removeState",
"params": { "path": "/todos", "index": 0 }
}
```
### `toggleState`
Toggle a boolean value at a state path:
```json
{
"action": "toggleState",
"params": { "path": "/sidebar/expanded" }
}
```
### `navigate`
Navigate to a URL. Requires a `navigate` callback on the `Renderer`:
```json
{
"action": "navigate",
"params": { "url": "/dashboard" }
}
```
```tsx
router.push(url)}
/>
```
## Custom Action Handlers
Register custom handlers via the `handlers` prop on `Renderer`. Custom handlers are merged with the built-in actions (custom handlers take precedence for name conflicts):
```tsx
import { Renderer } from "@prototyperco/compose"
import { prototyperComponents } from "@prototyperco/compose/components"
import type { ActionHandler } from "@prototyperco/compose/core"
const customHandlers: Record = {
submitForm: async (params, ctx) => {
const formData = ctx.getState("/form")
await fetch("/api/submit", {
method: "POST",
body: JSON.stringify(formData),
})
ctx.setState("/form/submitted", true)
},
addTodo: (params, ctx) => {
const title = params.title as string
ctx.setState("/todos", [
...((ctx.getState("/todos") as unknown[]) ?? []),
{ id: Date.now().toString(), title, done: false },
])
},
}
```
### ActionHandler Signature
```ts
type ActionHandler> = (
params: TParams,
ctx: ActionExecutionContext,
) => Promise | void;
```
### ActionExecutionContext
| Field | Type | Description |
| ---------- | ---------------------------------------- | ------------------------------------------------ |
| `getState` | `(path: string) => unknown` | Read a value from state by JSON Pointer path. |
| `setState` | `(path: string, value: unknown) => void` | Write a value to state by JSON Pointer path. |
| `navigate` | `(url: string) => void` | Navigate to a URL (if provided on the Renderer). |
## Confirmation Dialogs
Add a `confirm` field to show a confirmation dialog before executing the action:
```json
{
"action": "removeState",
"params": { "path": "/todos", "index": 0 },
"confirm": {
"title": "Delete Todo",
"message": "Are you sure you want to delete this item?",
"confirmLabel": "Delete",
"cancelLabel": "Keep"
}
}
```
The confirmation dialog is handled by the `onConfirm` callback on the `Renderer`:
```tsx
{
return window.confirm(`${confirm.title}\n${confirm.message}`);
}}
/>
```
`onConfirm` receives an `ActionConfirm` object and must return a `Promise`. If it resolves to `false`, the action is not executed.
### ActionConfirm Type
| Field | Type | Description |
| -------------- | --------- | -------------------------------------------------- |
| `title` | `string` | Dialog title. Supports `${/path}` interpolation. |
| `message` | `string?` | Optional dialog body text. Supports interpolation. |
| `confirmLabel` | `string?` | Label for the confirm button. |
| `cancelLabel` | `string?` | Label for the cancel button. |
## preventDefault
Set `preventDefault: true` on an action binding to call `event.preventDefault()` before the action handler runs. This is useful for suppressing default browser behavior such as form submission or link navigation:
```json
{
"action": "submitForm",
"preventDefault": true
}
```
A common use case is preventing a `` element's native submit and handling it entirely through Compose actions:
```json
{
"type": "Button",
"props": { "label": "Submit", "type": "submit" },
"on": {
"press": {
"action": "submitForm",
"params": { "formId": "contact" },
"preventDefault": true
}
}
}
```
When `preventDefault` is omitted or `false`, the browser's default behavior proceeds normally.
## Action Helpers
When building specs in TypeScript, the `actionBinding` helper provides shorthand constructors for common action binding patterns:
```ts
import { actionBinding } from "@prototyperco/compose/core";
// Simple action
actionBinding.simple("submitForm", { formId: "contact" });
// Action with confirmation dialog
actionBinding.withConfirm("deleteItem", {
title: "Delete?",
message: "This cannot be undone.",
});
// Action with success/error handlers
actionBinding.withSuccess("saveData", { navigate: "/dashboard" });
actionBinding.withError("saveData", { set: { "/error": "Save failed" } });
// Built-in action shorthands
actionBinding.setState("/form/submitted", true);
actionBinding.pushState("/todos", { id: "3", title: "New", done: false });
actionBinding.removeState("/todos", 0);
actionBinding.toggleState("/sidebar/expanded");
actionBinding.navigate("/dashboard");
```
See the [API Reference](/docs/compose/api-reference) for complete signatures.
## Action Chaining
Use `onSuccess` and `onError` to chain follow-up actions after the primary handler completes. Each accepts one of three variants:
### Navigate on success
Redirect the user after a successful action:
```json
{
"action": "submitForm",
"params": { "formId": "contact" },
"onSuccess": { "navigate": "/thank-you" }
}
```
### Set state values
Write one or more state values directly (no action handler needed):
```json
{
"action": "submitForm",
"params": { "formId": "contact" },
"onSuccess": {
"set": { "/form/status": "success", "/form/submitted": true }
},
"onError": { "set": { "/form/status": "error" } }
}
```
### Chain another action
Invoke a different action handler with its own parameters:
```json
{
"action": "submitForm",
"params": { "endpoint": "/api/contact" },
"onSuccess": {
"action": "showNotification",
"params": { "message": "Form submitted!" }
},
"onError": {
"action": "logError",
"params": { "source": "contactForm" }
}
}
```
### Summary of variants
| Variant | Shape | Available on |
| ------------ | ------------------------------------ | ---------------------- |
| Navigate | `{ navigate: "/path" }` | `onSuccess` only |
| Set state | `{ set: { "/key": value } }` | `onSuccess`, `onError` |
| Chain action | `{ action: "name", params?: {...} }` | `onSuccess`, `onError` |
**Execution flow:**
1. The primary action handler runs
2. On success: if `onSuccess` is defined, the follow-up runs (navigate, set state, or chain action)
3. On error: if `onError` is defined, the follow-up runs with the error context. If `onError` is not defined, the error is re-thrown.
## Dynamic Action Parameters
Action parameters can contain [expressions](/docs/compose/expressions) that are resolved before the handler is called:
```json
{
"action": "setState",
"params": {
"path": "/greeting",
"value": { "$template": "Hello, ${/user/name}!" }
}
}
```
State expressions and template interpolation in params:
```json
{
"action": "submitForm",
"params": {
"email": { "$state": "/form/email" },
"message": { "$template": "From ${/form/name}: ${/form/message}" }
}
}
```
In repeat contexts, `$item` in action params resolves to the **absolute state path** (not the value), allowing you to target specific array items:
```json
{
"action": "removeState",
"params": {
"path": "/todos",
"index": { "$index": true }
}
}
```
## Watch Events
The `watch` field on elements triggers side-effect actions when state values change:
```json
{
"type": "Card",
"props": {},
"children": ["content"],
"watch": ["/form/category"],
"on": {
"watch:/form/category": {
"action": "setState",
"params": { "path": "/form/subcategory", "value": "" }
}
}
}
```
When `/form/category` changes, the `watch:/form/category` event fires and the bound action runs. This is useful for cascading resets, dependent field updates, and similar side effects.
## Next Steps
- [Visibility](/docs/compose/visibility) — Show/hide elements based on state
- [Expressions](/docs/compose/expressions) — Dynamic values in props and params
- [Validation](/docs/compose/validation) — Field validation with built-in validators
# Compose API Reference
> Complete API reference for @prototyperco/compose
URL: https://prototyper-ui.com/docs/compose/api-reference
This is the full API reference for `@prototyperco/compose`, organized by import path.
## Main Entry
```ts
import { ... } from "@prototyperco/compose"
```
The main entry re-exports everything from `core`, `catalog`, and `react`, plus:
| Export | Type | Description |
| --------------------------- | ------------------- | ---------------------------------------------------------- |
| `prototyperComponents` | `ComponentRegistry` | Pre-built registry of 20 Prototyper UI component renderers |
| `specToJSX(spec, options?)` | Function | Convert a spec to copy-paste JSX code |
---
## Core
```ts
import { ... } from "@prototyperco/compose/core"
```
### Types
#### Spec & Elements
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `root` | `string` | - | Key of the root element |
| `elements` | `Record` | - | Flat map of elements by key |
| `state` | `Record` | - | Optional initial state model |
#### UIElement
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `type` | `string` | - | Component type from the catalog |
| `props` | `Record` | - | Component props (may contain dynamic expressions) |
| `children` | `string[]` | - | Child element keys (flat references) |
| `visible` | `VisibilityCondition` | - | Conditional rendering condition |
| `on` | `Record` | - | Event-to-action bindings |
| `repeat` | `RepeatBinding` | - | Repeat over items in a state array |
| `watch` | `string[]` | - | State paths to watch for changes |
#### Expression Types
All 8 expression types that can appear in prop values:
| Type | Shape | Description |
| --------------------- | ------------------------------ | --------------------------------------------------- |
| `StateExpression` | `{ $state: string }` | Read from global state by JSON Pointer path |
| `ItemExpression` | `{ $item: string }` | Read from current repeat item (`""` for whole item) |
| `IndexExpression` | `{ $index: true }` | Current repeat index (zero-based) |
| `BindStateExpression` | `{ $bindState: string }` | Two-way binding to global state path |
| `BindItemExpression` | `{ $bindItem: string }` | Two-way binding to repeat item field |
| `CondExpression` | `{ $cond, $then, $else? }` | Conditional value based on a visibility condition |
| `ComputedExpression` | `{ $computed: string, args? }` | Call a registered computed function |
| `TemplateExpression` | `{ $template: string }` | String interpolation with `${/path}` references |
The union type `Expression` includes all 8. `DynamicValue` is `T | Expression`. Convenience aliases: `DynamicString`, `DynamicNumber`, `DynamicBoolean`.
#### VisibilityCondition
```ts
type VisibilityCondition =
| boolean
| SingleCondition
| SingleCondition[] // implicit AND
| { $and: VisibilityCondition[] }
| { $or: VisibilityCondition[] };
```
`SingleCondition` is `StateCondition | ItemCondition | IndexCondition`, each supporting comparison operators: `eq`, `neq`, `gt`, `gte`, `lt`, `lte`, `not`.
#### RepeatBinding
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `source` | `string` | - | JSON Pointer path to a state array |
| `itemKey` | `string` | - | Field on each item to use as the React key |
#### ComparisonValue
A numeric comparison value used in visibility conditions. Can be a literal number or a dynamic state reference:
```ts
type ComparisonValue = number | { $state: string };
```
Used by the `gt`, `gte`, `lt`, and `lte` operators on visibility conditions and in the `visibility` helper functions.
#### ActionBinding
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `action` | `string` | - | Action handler name |
| `params` | `Record` | - | Parameters (may contain expressions) |
| `confirm` | `ActionConfirm` | - | Confirmation dialog before execution |
| `onSuccess` | `ActionOnSuccess` | - | Follow-up on success: navigate, set state, or chain action |
| `onError` | `ActionOnError` | - | Follow-up on error: set state or chain action |
| `preventDefault` | `boolean` | - | Call event.preventDefault() before executing the action |
#### ActionOnSuccess
Follow-up action to run on success. One of three variants:
```ts
type ActionOnSuccess =
| { navigate: string } // Redirect to a URL
| { set: Record } // Set state values
| { action: string; params?: Record> }; // Chain another action
```
#### ActionOnError
Follow-up action to run on error. One of two variants:
```ts
type ActionOnError =
| { set: Record } // Set state values
| { action: string; params?: Record> }; // Chain another action
```
#### FlatElement
A `UIElement` enriched with its own key and optional parent key. Useful for tree traversal and debugging:
```ts
interface FlatElement extends UIElement {
key: string;
parentKey?: string | null;
}
```
#### ActionConfirm
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `title` | `string` | - | Dialog title |
| `message` | `string` | - | Dialog message body |
| `confirmLabel` | `string` | - | Confirm button label |
| `cancelLabel` | `string` | - | Cancel button label |
#### ValidationCheck
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `validator` | `string` | - | Validator function name (built-in or custom) |
| `message` | `string` | - | Error message on failure |
| `args` | `Record>` | - | Arguments passed to the validator. Values can be literals or { $state } references for cross-field validation |
| `enabled` | `VisibilityCondition` | - | Condition for this check to run |
#### ValidationConfig
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `checks` | `ValidationCheck[]` | - | Array of validation checks |
| `validateOn` | `"change" \| "blur" \| "submit"` | `"change"` | When to run validation |
#### StateStore
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
#### JsonPatch
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `op` | `"add" \| "replace" \| "remove" \| "move" \| "copy" \| "test"` | - | RFC 6902 operation |
| `path` | `string` | - | JSON Pointer target path |
| `value` | `unknown` | - | Required for add, replace, test |
| `from` | `string` | - | Required for move, copy (source location) |
### Path Utilities
| Function | Signature | Description |
| ------------------ | ----------------------------------------------------- | -------------------------------------------- |
| `getByPath` | `(obj: unknown, path: string) => unknown` | Get a value by JSON Pointer path |
| `setByPath` | `(obj: Record, path: string, value: unknown) => void` | Set a value by path (mutating) |
| `addByPath` | `(obj: Record, path: string, value: unknown) => void` | Add per RFC 6902 semantics (array splice) |
| `removeByPath` | `(obj: Record, path: string) => void` | Remove per RFC 6902 semantics |
| `parseJsonPointer` | `(pointer: string) => string[]` | Parse a JSON Pointer into unescaped segments |
### Patch Utilities
| Function | Signature | Description |
| -------------- | ------------------------------------------------ | ----------------------------------------------- |
| `applyPatch` | `(target: Record, patch: JsonPatch) => void` | Apply a single RFC 6902 patch operation |
| `applyPatches` | `(target: Record, patches: JsonPatch[]) => void` | Apply multiple patches in order |
| `deepEqual` | `(a: unknown, b: unknown) => boolean` | Deep equality check (used by `test` operations) |
### State
| Function | Signature | Description |
| -------------------- | ------------------------------------------------------------------------------ | ----------------------------------------------------------------------- |
| `createStateStore` | `(initialState: Record) => StateStore` | Create an in-memory state store with change notification |
| `immutableSetByPath` | `(obj: Record, path: string, value: unknown) => Record` | Immutable set (returns new object) |
| `flattenToPointers` | `(obj: Record, prefix?: string, maxDepth?: number) => Record` | Flatten a nested object into a flat map of JSON Pointer paths to values |
### Store Adapter
Bridge an external state store (Zustand, Redux, Jotai, etc.) to the `StateStore` interface:
| Function | Signature | Description |
| -------------------- | -------------------------------------------- | ------------------------------------------------------ |
| `createStoreAdapter` | `(config: StoreAdapterConfig) => StateStore` | Create a StateStore adapter wrapping an external store |
#### StoreAdapterConfig
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `getSnapshot` | `() => StateModel` | - | Return the current state snapshot |
| `setSnapshot` | `(state: StateModel) => void` | - | Replace the state snapshot |
| `subscribe` | `(listener: () => void) => () => void` | - | Subscribe to state changes. Returns unsubscribe function |
### Prop Resolution
| Function | Signature | Description |
| --------------------- | ------------------------------------------------------------------------ | ------------------------------------------------------------ |
| `resolvePropValue` | `(value: unknown, ctx: PropResolutionContext) => unknown` | Resolve a single prop value (handles all 8 expression types) |
| `resolveElementProps` | `(props: Record, ctx: PropResolutionContext) => Record` | Resolve all props in an element |
| `resolveBindings` | `(props: Record, ctx?: PropResolutionContext) => Record` | Extract `$bindState`/`$bindItem` paths from props |
| `resolveActionParam` | `(value: unknown, ctx: PropResolutionContext) => unknown` | Resolve action param (`$item` yields path, not value) |
#### PropResolutionContext
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `stateModel` | `StateModel` | - | Global state model |
| `repeatItem` | `unknown` | - | Current repeat item |
| `repeatIndex` | `number` | - | Current repeat array index |
| `repeatBasePath` | `string` | - | Absolute state path to current repeat item |
| `functions` | `Record` | - | Named functions for $computed expressions |
Type guard functions are exported for each expression type: `isStateExpression`, `isItemExpression`, `isIndexExpression`, `isBindStateExpression`, `isBindItemExpression`, `isCondExpression`, `isComputedExpression`, `isTemplateExpression`.
### Visibility
| Function | Signature | Description |
| -------------------- | --------------------------------------------------------------------- | ------------------------------- |
| `evaluateVisibility` | `(condition: VisibilityCondition, ctx: VisibilityContext) => boolean` | Evaluate a visibility condition |
| `evaluateCondition` | `(condition: SingleCondition, ctx: VisibilityContext) => boolean` | Evaluate a single condition |
#### VisibilityContext
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `stateModel` | `StateModel` | - | Global state model |
| `repeatItem` | `unknown` | - | Current repeat item |
| `repeatIndex` | `number` | - | Current repeat index |
Type guards: `isStateCondition`, `isItemCondition`, `isIndexCondition`, `isAndCondition`, `isOrCondition`, `isSingleCondition`.
#### visibility.item
Convenience helpers for building `ItemCondition` objects inside repeat contexts:
| Method | Signature | Description |
| ------------------------ | ---------------------------------------------------------- | ----------------------------- |
| `visibility.item.when` | `(field: string) => ItemCondition` | Truthy check on an item field |
| `visibility.item.unless` | `(field: string) => ItemCondition` | Falsy check (not: true) |
| `visibility.item.eq` | `(field: string, value: unknown) => ItemCondition` | Item field equals value |
| `visibility.item.neq` | `(field: string, value: unknown) => ItemCondition` | Item field not equal |
| `visibility.item.gt` | `(field: string, value: ComparisonValue) => ItemCondition` | Greater than |
| `visibility.item.gte` | `(field: string, value: ComparisonValue) => ItemCondition` | Greater than or equal |
| `visibility.item.lt` | `(field: string, value: ComparisonValue) => ItemCondition` | Less than |
| `visibility.item.lte` | `(field: string, value: ComparisonValue) => ItemCondition` | Less than or equal |
#### visibility.index
Convenience helpers for building `IndexCondition` objects inside repeat contexts:
| Method | Signature | Description |
| ---------------------- | ----------------------------------- | --------------------------- |
| `visibility.index.eq` | `(value: number) => IndexCondition` | Index equals value |
| `visibility.index.neq` | `(value: number) => IndexCondition` | Index not equal |
| `visibility.index.gt` | `(value: number) => IndexCondition` | Index greater than |
| `visibility.index.gte` | `(value: number) => IndexCondition` | Index greater than or equal |
| `visibility.index.lt` | `(value: number) => IndexCondition` | Index less than |
| `visibility.index.lte` | `(value: number) => IndexCondition` | Index less than or equal |
### Actions
| Function | Signature | Description |
| ------------------- | -------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- |
| `resolveAction` | `(binding: ActionBinding, stateModel: StateModel) => ResolvedAction` | Resolve dynamic params and interpolate strings |
| `executeAction` | `(resolved: ResolvedAction, handlers: Record, ctx: ActionExecutionContext) => Promise` | Execute a resolved action with chaining |
| `interpolateString` | `(template: string, stateModel: StateModel) => string` | Replace `${/path}` references in a string |
| `builtInActions` | `Record` | Default handlers: `setState`, `pushState`, `removeState`, `toggleState`, `navigate` |
#### ActionExecutionContext
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `getState` | `(path: string) => unknown` | - | Read a value from state |
| `setState` | `(path: string, value: unknown) => void` | - | Write a value to state |
| `navigate` | `(url: string) => void` | - | Navigate to a URL (optional) |
#### Built-in Actions
| Action | Params | Description |
| ------------- | ----------------- | ---------------------------------- |
| `setState` | `{ path, value }` | Set a value at a state path |
| `pushState` | `{ path, value }` | Append to a state array |
| `removeState` | `{ path, index }` | Remove from a state array by index |
| `toggleState` | `{ path }` | Toggle a boolean at a state path |
| `navigate` | `{ url }` | Navigate via `ctx.navigate` |
### Action Helpers
The `actionBinding` object provides convenience constructors for building `ActionBinding` objects:
| Method | Signature | Description |
| --------------------------- | -------------------------------------------------------------------------------- | ---------------------------------- |
| `actionBinding.simple` | `(action: string, params?: Record) => ActionBinding` | Simple action with optional params |
| `actionBinding.withConfirm` | `(action: string, confirm: ActionConfirm, params?: Record) => ActionBinding` | Action with confirmation dialog |
| `actionBinding.withSuccess` | `(action: string, onSuccess: ActionOnSuccess, params?: Record) => ActionBinding` | Action with success handler |
| `actionBinding.withError` | `(action: string, onError: ActionOnError, params?: Record) => ActionBinding` | Action with error handler |
| `actionBinding.setState` | `(path: string, value: unknown) => ActionBinding` | Set a state value |
| `actionBinding.pushState` | `(path: string, value: unknown) => ActionBinding` | Append to a state array |
| `actionBinding.removeState` | `(path: string, index: unknown) => ActionBinding` | Remove from a state array by index |
| `actionBinding.toggleState` | `(path: string) => ActionBinding` | Toggle a boolean |
| `actionBinding.navigate` | `(url: string) => ActionBinding` | Navigate to a URL |
### Check Helpers
The `check` object provides convenience constructors for building `ValidationCheck` objects with sensible default messages:
| Method | Signature | Description |
| ------------------- | -------------------------------------------------------------------------- | ---------------------------------------- |
| `check.required` | `(message?: string) => ValidationCheck` | Required field |
| `check.email` | `(message?: string) => ValidationCheck` | Email format |
| `check.minLength` | `(min: number, message?: string) => ValidationCheck` | Minimum string/array length |
| `check.maxLength` | `(max: number, message?: string) => ValidationCheck` | Maximum string/array length |
| `check.pattern` | `(pattern: string, message?: string) => ValidationCheck` | Regex pattern match |
| `check.min` | `(min: number \| { $state: string }, message?: string) => ValidationCheck` | Minimum numeric value (supports dynamic) |
| `check.max` | `(max: number \| { $state: string }, message?: string) => ValidationCheck` | Maximum numeric value (supports dynamic) |
| `check.numeric` | `(message?: string) => ValidationCheck` | Must be a number |
| `check.url` | `(message?: string) => ValidationCheck` | URL format |
| `check.matches` | `(statePath: string, message?: string) => ValidationCheck` | Must match value at state path |
| `check.equalTo` | `(value: unknown, message?: string) => ValidationCheck` | Must equal a specific value |
| `check.lessThan` | `(statePath: string, message?: string) => ValidationCheck` | Must be less than value at state path |
| `check.greaterThan` | `(statePath: string, message?: string) => ValidationCheck` | Must be greater than value at state path |
| `check.requiredIf` | `(fieldPath: string, message?: string) => ValidationCheck` | Required when field is truthy |
### Validation
| Function | Signature | Description |
| -------------------- | ------------------------------------------------------------------------------------ | --------------------------------- |
| `runValidation` | `(checks, value, customValidators?, ctx?, evaluateEnabled?) => ValidationResult` | Run all checks and collect errors |
| `runValidationCheck` | `(check, value, customValidators?, ctx?, evaluateEnabled?) => ValidationCheckResult` | Run a single check |
| `builtInValidators` | `Record` | 14 built-in validator functions |
#### ValidationResult
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `valid` | `boolean` | - | Whether all checks passed |
| `errors` | `string[]` | - | Error messages from failed checks |
### Streaming
| Function / Type | Signature | Description |
| ---------------------- | ------------------------------------- | ------------------------------------------------- |
| `createStreamCompiler` | `() => StreamCompiler` | Create a stateful JSONL stream compiler |
| `parseStreamLine` | `(line: string) => JsonPatch \| null` | Parse a single JSONL line into a patch |
| `nestedToFlat` | `(tree: Record) => Spec` | Convert a nested element tree to flat spec format |
#### StreamCompiler
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
---
## React
```ts
import { ... } from "@prototyperco/compose/react"
// or from the main entry:
import { ... } from "@prototyperco/compose"
```
### Renderer
The top-level component that renders a spec.
```tsx
router.push(url)}
onConfirm={(confirm) => window.confirm(confirm.message)}
onStateChange={(state) => console.log(state)}
loading={isStreaming}
/>
```
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `spec` | `Spec \| null` | - | The UI spec to render. Renders nothing when null |
| `registry` | `ComponentRegistry` | - | Map of component type names to renderer functions |
| `handlers` | `Record` | - | Custom action handlers (merged with built-ins) |
| `functions` | `Record` | - | Named functions for $computed expressions |
| `navigate` | `(url: string) => void` | - | Navigation callback for the navigate action |
| `onConfirm` | `(confirm: ActionConfirm) => Promise` | - | Confirmation dialog handler |
| `onStateChange` | `(state: StateModel) => void` | - | Called on every state change |
| `loading` | `boolean` | - | Passed to component renderers during streaming |
### ComposeProvider
Composes all context providers. Use this when you need to render elements manually or access hooks outside the Renderer.
```tsx
```
Props are the same as `RendererProps` plus `children`, minus `loading`.
### ElementRenderer
Renders a single element by its key. Must be used inside a `ComposeProvider`.
```tsx
```
### ComponentRenderProps
Props passed to each component renderer function:
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `props` | `P` | - | Resolved prop values (expressions evaluated) |
| `children` | `ReactNode` | - | Rendered child elements |
| `emit` | `(event: string) => void` | - | Fire an event's action binding |
| `on` | `(event: string) => EventHandle` | - | Check if an event has a binding |
| `bindings` | `Record` | - | Map of prop names to bound state paths |
| `loading` | `boolean` | - | Whether the spec is still streaming |
### Hooks
#### useUIStream
Connect to a streaming JSONL endpoint:
```tsx
const { spec, isStreaming, error, send, clear } = useUIStream({
url: "/api/generate-ui",
method: "POST",
autoStart: false,
});
```
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `url` | `string` | - | Endpoint URL |
| `method` | `"GET" \| "POST"` | `"POST"` | HTTP method |
| `headers` | `Record` | - | Additional request headers |
| `body` | `unknown` | - | Default request body |
| `autoStart` | `boolean` | `false` | Start streaming on mount |
Returns:
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `spec` | `Spec \| null` | - | The progressively-built spec |
| `isStreaming` | `boolean` | - | Whether a stream is in progress |
| `error` | `Error \| null` | - | Last error, if any |
| `send` | `(body?: unknown) => void` | - | Start or restart the stream |
| `clear` | `() => void` | - | Abort stream and reset spec |
#### useBoundProp
Two-way binding for a single prop value:
```tsx
const [value, setValue] = useBoundProp(propValue, bindingPath);
```
When `bindingPath` is provided and the component is inside a `ComposeProvider`, reads/writes go through the state store. Otherwise, falls back to local state.
#### useStateBinding
Direct two-way binding to a state path:
```tsx
const [value, setValue] = useStateBinding("/counter");
```
Must be used inside a `ComposeProvider`.
#### useFieldValidation
Client-side field validation tied to a state path:
```tsx
const { errors, validate, touch, clear } = useFieldValidation("/form/email", {
checks: [
{ validator: "required", message: "Required" },
{ validator: "email", message: "Invalid email" },
],
validateOn: "change",
});
```
See the [Validation](/docs/compose/validation) page for full documentation.
### DevTools
#### ComposeDevTools
Debug overlay with spec tree, state inspector, and action log.
```tsx
```
See the [DevTools](/docs/compose/devtools) page for full documentation.
#### useDevToolsActionLog
Hook for external action logging:
```tsx
const { log, record, clear } = useDevToolsActionLog();
```
### Context Hooks
| Hook | Returns | Description |
| --------------------------- | ----------------------------------------------------- | ----------------------------------------------- |
| `useComposeState()` | `{ store, snapshot }` | Access the state store and current snapshot |
| `useComposeStateOptional()` | `{ store, snapshot } \| null` | Safe version that returns null outside provider |
| `useComposeActions()` | `ActionContextValue` | Access action handlers and execution |
| `useRepeatScope()` | `{ repeatItem, repeatIndex, repeatBasePath } \| null` | Access current repeat scope |
| `useComposeFunctions()` | `Record` | Access registered computed functions |
| `useSpecContext()` | `{ spec, registry }` | Access the current spec and component registry |
---
## Catalog
```ts
import { ... } from "@prototyperco/compose/catalog"
```
### Builder Functions
| Function | Signature | Description |
| ----------------- | -------------------------------------------------------------------- | -------------------------------------- |
| `defineComponent` | `(def: ComponentDefinition) => ComponentDefinition` | Define a component with type inference |
| `defineAction` | `(def: ActionDefinition) => ActionDefinition` | Define an action with type inference |
| `defineCatalog` | `(config: CatalogConfig) => Catalog` | Create a compiled catalog |
| `defineRegistry` | `(catalog: Catalog, componentMap: Record) => Registry` | Wire catalog to React renderers |
### Schema Utilities
| Function / Type | Signature | Description |
| --------------------------- | ------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- |
| `buildSpecZodSchema` | `(catalog: Catalog) => z.ZodType` | Build a Zod schema that validates a complete Spec against a catalog |
| `validateElementProps` | `(element: { type, props }, catalog: Catalog) => { valid, errors }` | Validate an element's literal props against its component schema (expressions are stripped) |
| `expressionSchema` | `z.ZodType` | Zod schema matching any of the 8 expression types |
| `visibilityConditionSchema` | `z.ZodType` | Zod schema matching any visibility condition |
| `dynamicOf` | `(baseSchema: z.ZodType) => z.ZodType` | Wrap a base schema to also accept expression objects |
### Type Inference
Type-level utilities for extracting TypeScript types from catalog definitions:
```ts
import type {
InferCatalogComponents,
InferCatalogActions,
InferComponentProps,
InferActionParams,
} from "@prototyperco/compose/catalog";
```
| Type | Description |
| --------------------------- | ------------------------------------------------------ |
| `InferCatalogComponents` | Extract the components map type from a Catalog |
| `InferCatalogActions` | Extract the actions map type from a Catalog |
| `InferComponentProps` | Extract the inferred props type for component name `K` |
| `InferActionParams` | Extract the inferred params type for action name `K` |
### Spec Builder
```ts
import { createSpecBuilder } from "@prototyperco/compose/catalog";
const builder = createSpecBuilder(catalog);
const { key, element } = builder.element(
"submitBtn",
"Button",
{ label: "Submit" },
{
on: { press: { action: "submitForm" } },
},
);
const spec = builder.spec("root", { root: rootElement, submitBtn: element });
```
| Function | Signature | Description |
| ------------------- | ----------------------------------- | ------------------------------------------------------------------ |
| `createSpecBuilder` | `(catalog: Catalog) => SpecBuilder` | Create a type-safe spec builder for programmatic spec construction |
### Prompt Generation
| Function | Signature | Description |
| ------------------- | --------------------------------------------------------- | ---------------------------------------- |
| `buildSystemPrompt` | `(catalog: Catalog, options?: PromptOptions) => string` | Generate a complete LLM system prompt |
| `buildUserPrompt` | `(prompt: string, options?: UserPromptOptions) => string` | Wrap a user prompt with optional context |
#### UserPromptOptions
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `currentSpec` | `Spec \| null` | - | Include current spec for refinement mode (patches only) |
| `stateContext` | `Record` | - | Application data for data-driven generation |
| `maxPromptLength` | `number` | - | Truncate the user's prompt text to this character length |
#### PromptOptions
Options for customizing prompt generation:
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `mode` | `"generate" \| "chat"` | `"generate"` | Prompt mode |
| `customRules` | `string[]` | - | Additional rules to include in the system prompt |
| `system` | `string` | - | Custom system prompt prefix |
| `template` | `PromptTemplate` | - | Custom template function for full prompt control |
#### PromptTemplate
A function that generates a prompt string from context:
```ts
type PromptTemplate = (context: PromptContext) => string;
```
#### PromptContext
Context passed to a prompt template function:
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `catalog` | `Catalog` | - | The catalog instance |
| `componentNames` | `string[]` | - | All registered component names |
| `actionNames` | `string[]` | - | All registered action names |
| `options` | `PromptOptions` | - | The prompt options |
| `formatZodType` | `(schema: z.ZodType) => string` | - | Utility to format a Zod type as a string |
| `defaultPrompt` | `string` | - | The default generated prompt (for use in custom templates) |
#### CatalogValidationResult
Result of `catalog.validate(spec)`:
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `valid` | `boolean` | - | Whether the spec is valid (no error-severity issues) |
| `data` | `Spec \| undefined` | - | The validated spec (present when valid) |
| `issues` | `SpecIssue[]` | - | All issues found (errors and warnings) |
#### SpecIssue
A single issue found during spec validation:
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `code` | `string` | - | Machine-readable issue code (e.g., "missing-root", "unknown-component") |
| `message` | `string` | - | Human-readable description |
| `severity` | `"error" \| "warning"` | - | Severity level |
| `elementKey` | `string?` | - | The element key where the issue was found |
| `path` | `string?` | - | Dot-separated path to the problematic field |
See the [Catalog](/docs/compose/catalog) page for full documentation.
---
## Codegen
```ts
import { specToJSX } from "@prototyperco/compose";
// or
import { specToJSX } from "@prototyperco/compose/codegen";
```
| Function | Signature | Description |
| ----------- | -------------------------------------------------- | --------------------------------- |
| `specToJSX` | `(spec: Spec, options?: CodegenOptions) => string` | Convert a spec to JSX source code |
#### CodegenOptions
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `indent` | `number` | `2` | Indentation size in spaces |
| `componentName` | `string` | `"GeneratedUI"` | Exported function name |
| `importPrefix` | `string` | `"@/registry/ui"` | Import path prefix for components |
See the [Code Export](/docs/compose/code-export) page for full documentation.
---
## Components
```ts
import { prototyperComponents } from "@prototyperco/compose/components";
```
`prototyperComponents` is a `ComponentRegistry` containing renderers for 20 Prototyper UI components:
Button, Card, CardContent, CardHeader, CardTitle, CardDescription, CardFooter, Heading, Text, Input, Textarea, Select, Checkbox, Switch, RadioGroup, Tabs, TabsList, TabsTrigger, TabsContent, Dialog, Accordion, Slider, Avatar, Separator, Progress, Tooltip, Badge, Alert.
Each renderer wraps the corresponding Prototyper UI component, mapping the `ComponentRenderProps` interface to the component's native props, handling two-way bindings, and forwarding events.
# Component Catalog
> Zod-based component definitions for AI-driven UI generation
URL: https://prototyper-ui.com/docs/compose/catalog
The catalog system lets you define your UI components with Zod schemas, then automatically generate LLM system prompts and JSON Schema exports. This is how the AI learns what components are available and how to use them.
## What is a Catalog?
A catalog is a structured registry of component definitions. Each component declares:
- A **Zod schema** for its props (validated at runtime)
- Supported **events** the component can emit
- Named **slots** for child content
- A human-readable **description** for prompt generation
- An optional **example** props object
When you build a system prompt from a catalog, the AI receives a precise description of every component it can use, including prop types, events, and usage examples.
## Defining Components
Use `defineComponent()` in `.catalog.ts` files to define individual components with full type inference:
```ts
import { z } from "zod";
import { defineComponent } from "@prototyperco/compose/catalog";
export default defineComponent({
description: "A clickable button that triggers an action",
props: z.object({
label: z.string().describe("Button text"),
variant: z.enum(["default", "destructive", "outline", "ghost"]).optional(),
size: z.enum(["sm", "md", "lg"]).optional(),
disabled: z.boolean().optional(),
}),
events: ["press"],
example: { label: "Click me", variant: "default" },
});
```
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `description` | `string` | - | Human-readable description for prompt generation |
| `props` | `z.ZodType` | - | Zod schema defining the component's props |
| `events` | `string[]` | - | Event names this component can emit (e.g., ["press", "change"]) |
| `slots` | `string[]` | - | Named slots for child content |
| `example` | `Record` | - | Example props object used in prompt generation |
## Defining Actions
Custom actions can also be defined with Zod schemas using `defineAction()`:
```ts
import { z } from "zod";
import { defineAction } from "@prototyperco/compose/catalog";
export const submitForm = defineAction({
description: "Submit the form data to the server",
params: z.object({
formId: z.string(),
validate: z.boolean().optional(),
}),
});
```
## Building a Catalog
Collect component and action definitions into a catalog with `defineCatalog()`:
```ts
import { defineCatalog } from "@prototyperco/compose/catalog";
import buttonDef from "./button.catalog";
import cardDef from "./card.catalog";
import inputDef from "./input.catalog";
import { submitForm } from "./actions";
const catalog = defineCatalog({
components: {
Button: buttonDef,
Card: cardDef,
Input: inputDef,
},
actions: {
submitForm,
},
});
```
The returned `Catalog` object provides several methods:
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `components` | `Record` | - | Map of component type names to their definitions |
| `actions` | `Record` | - | Map of action names to their definitions |
### Spec Validation
`catalog.validate(spec)` checks that:
- The spec has a valid `root` and `elements` structure
- The root element exists in the elements map
- Every element references a known component type
- Element props pass Zod schema validation
- Child references point to existing elements
- Element-level keys (`visible`, `on`, `repeat`, `watch`) are not misplaced inside props
```ts
const result = catalog.validate(spec);
if (!result.valid) {
for (const issue of result.issues) {
console.error(`[${issue.severity}] ${issue.code}: ${issue.message}`);
}
}
```
See the [Enhanced Validation](#enhanced-validation) section below for details on the structured result format.
## Full Spec Schema
The catalog can generate a Zod schema or JSON Schema for the entire spec format, not just individual components. This is useful for structured outputs from LLMs and for validating complete specs at the boundary.
### zodSchema()
Returns a Zod schema that validates a complete `Spec` object against the catalog. Component `type` fields are constrained to the catalog's registered component names.
```ts
const schema = catalog.zodSchema();
// Use for runtime validation
const result = schema.safeParse(specFromLLM);
if (!result.success) {
console.error(result.error.issues);
}
```
### jsonSchema()
Returns the full spec schema in JSON Schema format. This is ideal for structured output modes in LLM APIs (e.g., OpenAI's `response_format` or Anthropic's tool use):
```ts
const jsonSchema = catalog.jsonSchema()
// Pass to an LLM API as the response schema
const response = await openai.chat.completions.create({
model: "gpt-4o",
messages: [...],
response_format: {
type: "json_schema",
json_schema: { name: "ui_spec", schema: jsonSchema },
},
})
```
The schema includes the full spec structure (root, elements, state), all expression types, visibility conditions, action bindings, and validation configs.
For more on using structured outputs with the catalog, see the [Prompt Generation](#prompt-generation) section.
## Enhanced Validation
`catalog.validate(spec)` returns a `CatalogValidationResult` with structured issues instead of a flat error string array. Each issue includes a machine-readable code, severity level, and optional element key for precise error reporting.
```ts
const result = catalog.validate(spec);
if (!result.valid) {
for (const issue of result.issues) {
console.log(`[${issue.severity}] ${issue.code}: ${issue.message}`);
// e.g. [error] unknown-component: Element 'header' uses unknown component type 'Header'
// e.g. [warning] misplaced-visible: Element 'card' has 'visible' inside props
}
}
// On success, result.data contains the validated Spec
if (result.valid && result.data) {
renderSpec(result.data);
}
```
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `valid` | `boolean` | - | Whether the spec is valid (no error-severity issues) |
| `data` | `Spec \| undefined` | - | The validated spec (present when valid) |
| `issues` | `SpecIssue[]` | - | All issues found (both errors and warnings) |
Each `SpecIssue` has:
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `code` | `string` | - | Machine-readable code (e.g., "missing-root", "unknown-component", "invalid-props") |
| `message` | `string` | - | Human-readable description |
| `severity` | `"error" \| "warning"` | - | Severity level — only errors cause valid to be false |
| `elementKey` | `string?` | - | The element key where the issue was found |
| `path` | `string?` | - | Dot-separated path to the problematic field |
Issue codes include: `missing-root`, `root-not-found`, `unknown-component`, `invalid-props`, `missing-child`, `orphaned-element`, `misplaced-visible`, `misplaced-on`, `misplaced-repeat`, `misplaced-watch`.
## Component Schemas
`catalog.componentSchemas()` returns per-component JSON Schemas (the behavior of the previous `jsonSchema()` method). Use this when you need individual component prop schemas rather than the full spec schema:
```ts
const schemas = catalog.componentSchemas();
// {
// "$schema": "http://json-schema.org/draft-07/schema#",
// "components": {
// "Button": { "type": "object", "properties": { ... } },
// "Card": { ... },
// },
// "actions": {
// "submitForm": { ... },
// },
// }
```
This is useful for documentation generation, API endpoint definitions, or when you need to validate props for a single component type.
## Wiring to React Renderers
Use `defineRegistry()` to connect a catalog to React component renderers:
```ts
import { defineRegistry } from "@prototyperco/compose/catalog";
import { Button } from "./components/Button";
import { Card } from "./components/Card";
import { Input } from "./components/Input";
const { registry, catalog } = defineRegistry(catalog, {
Button,
Card,
Input,
});
```
The function warns at runtime if:
- A catalog component has no renderer in the map
- A renderer key is not in the catalog
The returned `registry` object can be passed directly to the `` component.
## Prompt Generation
### buildSystemPrompt()
Generate a complete system prompt from a catalog. The prompt describes:
- The JSONL streaming output format (RFC 6902 JSON Patch)
- The flat spec structure (root, elements, state)
- All dynamic value expressions (`$state`, `$bindState`, `$item`, `$cond`, etc.)
- Repeat/list rendering
- Visibility conditions
- Event and action bindings
- Every registered component with its prop types, events, and slots
- Custom actions with their parameter schemas
- Rules the AI must follow
```ts
import { buildSystemPrompt } from "@prototyperco/compose/catalog";
const systemPrompt = buildSystemPrompt(catalog);
// Pass as the system message to your AI model
```
The prompt includes a concrete streaming example built from the first two components in your catalog, so the AI sees the exact output format.
### buildUserPrompt()
Wrap a user's request with optional context:
```ts
import { buildUserPrompt } from "@prototyperco/compose/catalog";
// Simple prompt
const prompt = buildUserPrompt(
"Build a login form with email and password fields",
);
// Refinement mode — include current spec so the AI outputs patches only
const refinementPrompt = buildUserPrompt("Add a forgot password link", {
currentSpec: existingSpec,
});
// With state context — provide data for data-driven generation
const dataPrompt = buildUserPrompt("Show a table of these users", {
stateContext: { users: [{ id: 1, name: "Alice" }] },
});
```
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `currentSpec` | `Spec` | - | When provided, instructs the model to output only patches (refinement mode) |
| `stateContext` | `Record` | - | Application data to populate the UI via $state expressions |
In refinement mode, the prompt includes the full current spec as JSON and instructs the model to output only RFC 6902 patches to modify it, rather than regenerating the entire spec.
## The Build Pipeline
In a typical project, `.catalog.ts` files live alongside your component source files:
```
registry/ui/
button.tsx # React component
button.catalog.ts # Catalog definition
card.tsx
card.catalog.ts
input.tsx
input.catalog.ts
```
A build script collects all `.catalog.ts` files, calls `defineCatalog()`, and outputs:
1. **JSON Schema** — exported via an API route (e.g., `/compose/catalog.json`)
2. **System prompt** — exported via a text endpoint (e.g., `/compose/prompt.txt`)
3. **Registry** — wired to renderers and used by `` at runtime
This separation means the AI prompt always stays in sync with your actual component definitions.
## Full Example
Putting it all together:
```ts
import { z } from "zod";
import {
defineComponent,
defineAction,
defineCatalog,
defineRegistry,
buildSystemPrompt,
buildUserPrompt,
} from "@prototyperco/compose/catalog";
// Define components
const Button = defineComponent({
description: "A clickable button",
props: z.object({
label: z.string(),
variant: z.enum(["default", "destructive"]).optional(),
}),
events: ["press"],
example: { label: "Submit" },
});
const Input = defineComponent({
description: "A text input field",
props: z.object({
label: z.string().optional(),
placeholder: z.string().optional(),
type: z.enum(["text", "email", "password"]).optional(),
}),
events: ["change"],
example: { label: "Email", type: "email" },
});
// Define custom actions
const submitForm = defineAction({
description: "Validate and submit form data",
params: z.object({ formId: z.string() }),
});
// Build catalog
const catalog = defineCatalog({
components: { Button, Input },
actions: { submitForm },
});
// Generate prompts
const systemPrompt = buildSystemPrompt(catalog);
const userPrompt = buildUserPrompt("Create a login form");
// Wire to React renderers
const { registry } = defineRegistry(catalog, {
Button: ButtonRenderer,
Input: InputRenderer,
});
```
# Code Export
> Convert specs to copy-paste React/JSX code
URL: https://prototyper-ui.com/docs/compose/code-export
The `specToJSX()` function converts any Compose spec into a self-contained React component file. This lets users go from AI-generated UI to real, editable code in one step.
## Basic Usage
```ts
import { specToJSX } from "@prototyperco/compose";
const spec = {
root: "card",
elements: {
card: { type: "Card", props: {}, children: ["heading", "btn"] },
heading: { type: "Heading", props: { text: "Hello World", level: 2 } },
btn: { type: "Button", props: { label: "Click me" } },
},
};
const code = specToJSX(spec);
```
Output:
```tsx
"use client";
import { Card } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
export function GeneratedUI() {
return (
Hello World
Click me
);
}
```
## Options
Pass a `CodegenOptions` object to customize the output:
```ts
const code = specToJSX(spec, {
indent: 4,
componentName: "LoginForm",
importPrefix: "@/components/ui",
});
```
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `indent` | `number` | `2` | Indentation size in spaces |
| `componentName` | `string` | `"GeneratedUI"` | Exported function name |
| `importPrefix` | `string` | `"@/registry/ui"` | Import path prefix for components |
## How Components Map to Imports
Each component type maps to an import file. For example, `Button` imports from `{prefix}/button`, `CardTitle` imports from `{prefix}/card`, and `SelectItem` imports from `{prefix}/select`. Multiple components from the same file are grouped into a single import statement.
Two component types map to HTML elements instead of imports:
- `Heading` renders as `` through `` based on the `level` prop
- `Text` renders as ` `
## Dynamic Value Conversion
### State Expressions
`$state` expressions convert to variable references using camelCase naming derived from the JSON Pointer path:
```json
{ "text": { "$state": "/user/name" } }
```
```tsx
{
userName;
}
```
### Two-Way Bindings
`$bindState` expressions generate `value` + `onChange` pairs appropriate to the component type:
| Component | Generated Code |
| ------------------ | ---------------------------------------------------------- |
| Input, Textarea | `value={email} onChange={(e) => setEmail(e.target.value)}` |
| Checkbox, Switch | `checked={agreed} onCheckedChange={setAgreed}` |
| Select, RadioGroup | `value={role} onValueChange={setRole}` |
| Slider | `value={[volume]} onValueChange={([v]) => setVolume(v)}` |
### Template Expressions
`$template` strings convert to template literals:
```json
{ "content": { "$template": "Hello ${/name}!" } }
```
```tsx
{
`Hello ${name}!`;
}
```
### Computed and Conditional Expressions
`$computed` and `$cond` expressions generate placeholder comments since their logic cannot be statically resolved:
```tsx
{
/* computed: formatDate */
}
{
/* conditional value */
}
```
### Item and Index Expressions
Inside repeated elements, `$item` and `$index` expressions reference the `.map()` callback parameters:
```tsx
{
item.title;
}
{
index;
}
```
## State Management
When the spec includes a `state` object, `specToJSX` generates `useState` hooks for each top-level state key:
```json
{ "state": { "count": 0, "items": [], "name": "World" } }
```
```tsx
import { useState } from "react";
export function GeneratedUI() {
const [count, setCount] = useState(0);
const [items, setItems] = useState([]);
const [name, setName] = useState("World");
// ...
}
```
## Actions
Event bindings in the `on` field convert to inline handlers:
| Action | Generated Code |
| -------------- | -------------------------------------------------------- |
| `setState` | `setCount(5)` or `setName(otherVar)` |
| `toggleState` | `setActive((prev) => !prev)` |
| `appendItem` | `setItems((prev) => [...prev, newItem])` |
| `removeItem` | `setItems((prev) => prev.filter((_, i) => i !== index))` |
| Custom actions | Comment with action name and params |
The `press` event maps to `onClick` in React.
## Repeat (Lists)
Elements with a `repeat` binding generate `.map()` calls:
```json
{
"type": "Card",
"repeat": { "source": "/todos", "itemKey": "id" },
"children": ["todoItem"]
}
```
```tsx
{
todos.map((item) => {/* children */} );
}
```
## Visibility (Conditional Rendering)
Elements with `visible` conditions generate conditional expressions:
```json
{
"type": "Text",
"props": { "content": "Loading..." },
"visible": { "$state": "/isLoading" }
}
```
```tsx
{
isLoading &&
Loading...
;
}
```
Complex conditions use the appropriate operators:
```json
{ "visible": { "$state": "/count", "gt": 0 } }
```
```tsx
{count > 0 && (
// ...
)}
```
## Limitations
The code export is designed to produce a working starting point, not a perfect final result. Some dynamic behavior cannot be statically converted:
- **`$computed` expressions** become comments since the function implementations live outside the spec
- **`$cond` expressions** become placeholder comments
- **Custom actions** (not `setState`/`toggleState`/`appendItem`/`removeItem`) generate comment stubs
- **Cross-field validation** is not included in the exported code
- **State is flat** — nested state paths are converted to flat `useState` calls at the top level
The intended workflow is: AI generates a spec, the user previews it live with the Renderer, and then exports to code for further customization.
## Use Case
```
AI Model --> Streaming Spec --> Live Preview (Renderer)
|
[Export]
|
JSX Component --> Customize & Ship
```
The exported code uses your project's actual component imports, so it integrates directly into your codebase without any Compose runtime dependency.
# DevTools
> Debug overlay for inspecting specs, state, and actions
URL: https://prototyper-ui.com/docs/compose/devtools
Compose includes a built-in developer tools panel for inspecting the spec tree, watching live state, and tracking action events. No additional packages needed.
## Setup
Mount ` ` inside your `` (or alongside ``, which creates one internally). Conditionally render it for development only:
```tsx
import { Renderer, ComposeDevTools } from "@prototyperco/compose";
import { prototyperComponents } from "@prototyperco/compose/components";
function App() {
return (
{process.env.NODE_ENV === "development" && }
);
}
```
When used with `ComposeProvider` directly:
```tsx
import {
ComposeProvider,
ElementRenderer,
ComposeDevTools,
} from "@prototyperco/compose";
function App() {
return (
{process.env.NODE_ENV === "development" && }
);
}
```
The DevTools render as a fixed panel at the bottom of the viewport. Click the "Compose" tab to open it.
## Tabs
### Spec Tree
Displays the element hierarchy as an interactive tree. Each node shows:
- **Key** — the element's string key (e.g., `"card"`, `"heading"`)
- **Type** — the component type in angle brackets (e.g., ``)
- **Badges** — indicators for `root`, `children` count, `events`, `repeat`, and `conditional` visibility
Click any node to inspect its details in the right pane:
- **Props** — the element's raw props object (before expression resolution)
- **Events** — the `on` binding map (if any)
- **Visibility** — the `visible` condition (if any)
- **Repeat** — the `repeat` configuration (if any)
### State
Shows the full live state model as formatted JSON. Updates in real-time as state changes through actions, two-way bindings, or direct `store.set()` calls.
### Action Log
A timestamped log of every action executed. Each entry shows:
- **Timestamp** — when the action fired
- **Action name** — the action that was called (e.g., `setState`, `pushState`)
- **Params** — the resolved parameter values
Use the **Clear** button to reset the log.
## Props
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `defaultOpen` | `boolean` | `false` | Whether the panel starts open |
| `actionLog` | `ActionLogEntry[]` | - | Pre-supplied action log entries for external control |
## External Action Logging
By default, the DevTools manage their own internal action log. For external control (e.g., if you want to log actions from custom handlers), use the `useDevToolsActionLog()` hook:
```tsx
import { ComposeDevTools, useDevToolsActionLog } from "@prototyperco/compose";
function App() {
const { log, record, clear } = useDevToolsActionLog();
// Call `record` whenever a custom action fires
function handleCustomAction(name: string, params: Record) {
record(name, params);
// ... execute the action
}
return (
handleCustomAction("myAction", params),
}}
/>
);
}
```
### useDevToolsActionLog() Return Value
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `log` | `ActionLogEntry[]` | - | The current action log array |
| `record` | `(action: string, params?: Record) => void` | - | Record a new action entry |
| `clear` | `() => void` | - | Clear all log entries |
### ActionLogEntry
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `id` | `number` | - | Unique entry identifier |
| `timestamp` | `number` | - | Unix timestamp (Date.now()) |
| `action` | `string` | - | The action name |
| `params` | `Record` | - | Resolved action parameters |
## Production Builds
The DevTools panel is designed for development only. Always wrap it in a condition to exclude it from production bundles:
```tsx
{
process.env.NODE_ENV === "development" && ;
}
```
If you use Next.js, the dead-code elimination in production builds will strip the DevTools component entirely when wrapped in this check.
# Dynamic Expressions
> Data binding, conditional values, and string interpolation in Compose specs
URL: https://prototyper-ui.com/docs/compose/expressions
Expressions are special objects in prop values that are resolved at render time. They connect your UI to state, repeat contexts, and computed logic. Instead of static values, any prop can contain an expression that dynamically resolves based on the current state.
## Expression Types
Compose supports 8 expression types:
| Expression | Syntax | Direction | Description |
| ------------ | ---------------------------------------------- | ------------ | ----------------------------------------- |
| `$state` | `{ "$state": "/path" }` | Read | Read a value from global state |
| `$item` | `{ "$item": "field" }` | Read | Read a field from the current repeat item |
| `$index` | `{ "$index": true }` | Read | Current repeat index (zero-based) |
| `$bindState` | `{ "$bindState": "/path" }` | Read + Write | Two-way binding to a state path |
| `$bindItem` | `{ "$bindItem": "field" }` | Read + Write | Two-way binding to a repeat item field |
| `$cond` | `{ "$cond": ..., "$then": ..., "$else": ... }` | Read | Conditional value selection |
| `$computed` | `{ "$computed": "fnName", "args": {} }` | Read | Call a registered function |
| `$template` | `{ "$template": "Hello ${/name}!" }` | Read | String interpolation |
## `$state` — Read from State
Read a value from the global state model using a JSON Pointer path:
```json
{
"type": "Text",
"props": {
"content": { "$state": "/user/name" }
}
}
```
With state `{ "user": { "name": "Alice" } }`, the `content` prop resolves to `"Alice"`.
Nested paths work with standard JSON Pointer syntax:
```json
{ "$state": "/settings/theme/mode" }
{ "$state": "/items/0/title" }
```
If the path does not exist, the expression resolves to `undefined`.
## `$item` — Read from Repeat Item
Inside a [repeat](/docs/compose/spec-format#repeat-binding) scope, read a field from the current array item:
```json
{
"type": "Text",
"props": {
"content": { "$item": "title" }
}
}
```
If the repeat source is `/todos` and the current item is `{ "title": "Buy milk", "done": false }`, this resolves to `"Buy milk"`.
Use an empty string to get the entire item:
```json
{ "$item": "" }
```
Dot-separated paths are supported for nested fields:
```json
{ "$item": "address.city" }
```
## `$index` — Current Repeat Index
Returns the zero-based index of the current item within a repeat scope:
```json
{
"type": "Badge",
"props": {
"label": { "$index": true }
}
}
```
To use the index in a string, combine with `$template` and reference the index via `$cond` or concatenation — note that `$template` interpolates _state paths_, not the repeat index directly. For display strings like "Item #1", use `$computed` or build labels from `$index`:
```json
{
"type": "Text",
"props": {
"content": {
"$computed": "formatIndex",
"args": { "index": { "$index": true } }
}
}
}
```
## `$bindState` — Two-Way State Binding
Creates a two-way binding between a prop and a state path. The prop reads the current value from state, and when the component updates the value (e.g. user types in an input), the state is automatically written back:
```json
{
"type": "Input",
"props": {
"label": "Email",
"value": { "$bindState": "/form/email" }
}
}
```
This is the primary mechanism for form inputs. The component receives both the current value and a callback to update it. No explicit `on.change` action is needed for basic value synchronization.
`$bindState` is equivalent to `$state` for reads. The difference is that the renderer also extracts the path into a `bindings` map, allowing the component wrapper to set up a write-back channel.
## `$bindItem` — Two-Way Repeat Item Binding
The repeat-scoped equivalent of `$bindState`. Binds a prop to a field on the current repeat item:
```json
{
"type": "Checkbox",
"props": {
"checked": { "$bindItem": "done" }
}
}
```
Inside a repeat over `/todos`, for the item at index 2, this resolves to the absolute state path `/todos/2/done` for writes, and reads the current value of that field.
## `$cond` — Conditional Values
Select between two values based on a condition. The `$cond` field uses the same [visibility condition](/docs/compose/visibility) system:
```json
{
"type": "Badge",
"props": {
"variant": {
"$cond": { "$state": "/user/isAdmin" },
"$then": "default",
"$else": "secondary"
}
}
}
```
The condition is evaluated using the visibility engine. If it passes, `$then` is returned; otherwise `$else` (which defaults to `undefined` if omitted).
Both `$then` and `$else` can themselves be expressions, enabling nested conditionals:
```json
{
"$cond": { "$state": "/status", "eq": "error" },
"$then": "destructive",
"$else": {
"$cond": { "$state": "/status", "eq": "success" },
"$then": "default",
"$else": "secondary"
}
}
```
You can use comparison operators in the condition:
```json
{
"$cond": { "$state": "/cart/total", "gt": 100 },
"$then": "Free shipping!",
"$else": { "$template": "Add ${/remaining} more for free shipping" }
}
```
## `$computed` — Custom Functions
Call a named function registered on the `Renderer`:
```json
{
"type": "Text",
"props": {
"content": {
"$computed": "formatCurrency",
"args": { "amount": { "$state": "/cart/total" }, "currency": "USD" }
}
}
}
```
Register the function via the `functions` prop:
```tsx
new Intl.NumberFormat("en-US", { style: "currency", currency }).format(
amount as number,
),
uppercase: ({ text }) => String(text).toUpperCase(),
}}
/>
```
Arguments in `args` are themselves resolved as expressions before being passed to the function. This means you can use `$state`, `$item`, or any other expression inside `args`.
## `$template` — String Interpolation
Interpolate state values into a template string. References use `${/json/pointer/path}` syntax:
```json
{
"type": "Text",
"props": {
"content": {
"$template": "Hello, ${/user/name}! You have ${/notifications/count} new messages."
}
}
}
```
With state `{ "user": { "name": "Alice" }, "notifications": { "count": 5 } }`, this resolves to `"Hello, Alice! You have 5 new messages."`.
- Paths must start with `/` (JSON Pointer format)
- Missing values resolve to an empty string
- Non-string values are coerced via `String()`
## When to Use Each Expression
| Scenario | Expression | Example |
| ------------------------ | ------------ | --------------------------------------- |
| Display a state value | `$state` | Show user's name in a heading |
| Bind a form input | `$bindState` | Input value synced to state |
| Display repeat item data | `$item` | Show each todo's title in a list |
| Edit repeat item data | `$bindItem` | Checkbox for each todo's "done" field |
| Show item number | `$index` | "Item #1", "Item #2", etc. |
| Toggle variant/style | `$cond` | Red badge for errors, green for success |
| Format/compute values | `$computed` | Currency formatting, date formatting |
| Build dynamic strings | `$template` | "Welcome back, Alice!" |
## Resolution Order
When the renderer encounters a prop value, it checks for expressions in this order:
1. `$state` — read from global state
2. `$item` — read from repeat item
3. `$index` — return repeat index
4. `$bindState` — read from global state (also extracts binding path)
5. `$bindItem` — read from repeat item (also extracts binding path)
6. `$cond` — evaluate condition and resolve `$then` or `$else`
7. `$computed` — call registered function
8. `$template` — interpolate string
Plain objects that do not match any expression pattern are recursively resolved (each value is checked for expressions). Arrays are resolved element-by-element. Primitives (`string`, `number`, `boolean`, `null`) pass through unchanged.
## Next Steps
- [Visibility](/docs/compose/visibility) — Conditional rendering using the same condition system as `$cond`
- [Actions](/docs/compose/actions) — Using expressions in action parameters
- [Spec Format](/docs/compose/spec-format) — The full spec structure including repeat bindings
# Getting Started with Compose
> Set up Compose in your Next.js project in under 2 minutes
URL: https://prototyper-ui.com/docs/compose/getting-started
## Quick Start
The fastest way to add Compose to your project is with the CLI scaffold:
### 1. Scaffold
```bash
bunx @prototyperco/cli add compose
```
This creates two files and adds your API key placeholder to `.env.local`:
- `app/api/compose/route.ts` — server handler that streams UI specs from an AI model
- `app/compose/page.tsx` — ready-to-use page with the ` ` component
### 2. Add your API key
Open `.env.local` and paste your Anthropic key:
```bash
ANTHROPIC_API_KEY=sk-ant-your-key-here
```
### 3. Start the dev server
```bash
npm run dev
```
Open [http://localhost:3000/compose](http://localhost:3000/compose) and describe a UI.
---
## What was scaffolded
The CLI creates a minimal but complete setup:
**`app/api/compose/route.ts`** — A Next.js API route that calls an AI model and streams back JSONL patches:
```ts
import { createComposeHandler } from "@prototyperco/compose/server";
export const POST = createComposeHandler({
provider: "anthropic",
});
```
**`app/compose/page.tsx`** — A client page that renders the streamed UI:
```tsx
"use client";
import { Compose } from "@prototyperco/compose/react";
export default function ComposePage() {
return (
Compose
Describe a UI and watch it appear in real-time.
);
}
```
---
## The Compose Component
` ` is an all-in-one component that handles prompt input, streaming, spec assembly, and rendering. It connects to your API route and manages the full lifecycle.
```tsx
import { Compose } from "@prototyperco/compose/react";
;
```
### Props
| Prop | Type | Default | Description |
| ------------- | ------------------------ | -------------------- | ------------------------------------------- |
| `endpoint` | `string` | — | **Required.** URL of the compose API route. |
| `placeholder` | `string` | `"Describe a UI..."` | Placeholder text for the prompt input. |
| `onSpec` | `(spec: Spec) => void` | — | Called when a new spec is received. |
| `onError` | `(error: Error) => void` | — | Called on streaming errors. |
| `className` | `string` | — | Additional CSS classes for the wrapper. |
---
## Server Handler
`createComposeHandler` creates a Next.js-compatible `POST` handler. It builds a system prompt from the component catalog, calls the AI provider, and streams JSONL patches back to the client.
```ts
import { createComposeHandler } from "@prototyperco/compose/server";
export const POST = createComposeHandler({
provider: "anthropic",
model: "claude-sonnet-4-20250514", // optional, this is the default
maxTokens: 4096, // optional
rules: ["Use a dark color scheme"], // optional extra instructions
});
```
| Option | Type | Description |
| ----------------- | ------------------------------ | ------------------------------------------------------------------------------------------ |
| `provider` | `"anthropic" \| "openai"` | **Required.** Which AI provider to use. |
| `apiKey` | `string` | API key. Falls back to `ANTHROPIC_API_KEY` or `OPENAI_API_KEY` env vars. |
| `model` | `string` | Model identifier. Defaults to `claude-sonnet-4-20250514` (Anthropic) or `gpt-4o` (OpenAI). |
| `maxTokens` | `number` | Max tokens for the AI response. Default `4096`. |
| `rules` | `string[]` | Additional rules appended to the system prompt. |
| `systemPrompt` | `string` | Full system prompt override (bypasses catalog-based generation). |
| `mode` | `"generate" \| "chat"` | Prompt mode. `"generate"` outputs raw JSONL; `"chat"` wraps in code fences. |
| `onRequest` | `(params) => override \| void` | Hook to modify prompts before sending to AI. |
| `onResponse` | `(params) => void` | Hook called after the AI response completes. |
| `maxPromptLength` | `number` | Truncate user prompts to this character length. |
---
## Using OpenAI
To use OpenAI instead of Anthropic:
```bash
bunx @prototyperco/cli add compose --provider openai
```
Or change the route manually:
```ts
import { createComposeHandler } from "@prototyperco/compose/server";
export const POST = createComposeHandler({
provider: "openai",
});
```
Set the OpenAI key in `.env.local`:
```bash
OPENAI_API_KEY=sk-your-openai-key-here
```
---
## Manual Setup
If you prefer to set things up yourself instead of using the CLI scaffold:
```bash
pnpm add @prototyperco/compose
```
Compose requires `react@^19` and `react-dom@^19` as peer dependencies.
Create the two files shown in [What was scaffolded](#what-was-scaffolded) above, then add your API key to `.env.local`.
---
## Low-level API
For more control, use the lower-level hooks and renderer directly:
```tsx
"use client";
import { Renderer, useUIStream } from "@prototyperco/compose";
import { prototyperComponents } from "@prototyperco/compose/components";
export function CustomCompose() {
const { spec, isStreaming, error, send, clear } = useUIStream({
url: "/api/compose",
});
return (
send({ prompt: "Build a login form" })}>
Generate
{error &&
Error: {error.message}
}
{spec && (
)}
);
}
```
The `Renderer` component accepts these props:
| Prop | Type | Description |
| --------------- | ---------------------------------------------- | -------------------------------------------------------- |
| `spec` | `Spec \| null` | The UI spec to render. Pass `null` to render nothing. |
| `registry` | `ComponentRegistry` | Map of component type names to renderer functions. |
| `handlers` | `Record` | Custom action handlers (merged with built-in actions). |
| `functions` | `Record` | Named functions for `$computed` expressions. |
| `navigate` | `(url: string) => void` | Navigation callback for the `navigate` action. |
| `onConfirm` | `(confirm: ActionConfirm) => Promise` | Confirmation dialog handler. |
| `onStateChange` | `(state: StateModel) => void` | Called whenever state changes. |
| `loading` | `boolean` | Passed through to components (e.g. for skeleton states). |
---
## Available Components
Compose includes wrappers for 20 Prototyper UI components:
| Category | Components |
| ---------------- | ------------------------------------------------------------- |
| **Actions** | Button |
| **Forms** | Input, Textarea, Select, Checkbox, Switch, RadioGroup, Slider |
| **Overlays** | Dialog, Tooltip |
| **Navigation** | Tabs |
| **Layout** | Card, Accordion, Separator |
| **Data Display** | Heading, Text, Badge, Avatar, Alert |
| **Feedback** | Progress |
Import the full registry:
```tsx
import { prototyperComponents } from "@prototyperco/compose/components";
```
Or import individual wrappers to build a custom registry:
```tsx
import { buttonRenderer } from "@prototyperco/compose/components";
const myRegistry = {
Button: buttonRenderer,
// ... add only what you need
};
```
---
## Next Steps
- [Spec Format](/docs/compose/spec-format) — Deep dive into the spec structure
- [Expressions](/docs/compose/expressions) — Dynamic values and data binding
- [Streaming](/docs/compose/streaming) — Connect to any AI model
- [Actions](/docs/compose/actions) — Handle user interactions
# DX Helpers
> Convenience builders for actions, validation checks, and visibility conditions.
URL: https://prototyper-ui.com/docs/compose/helpers
Compose specs are plain JSON objects, so you can always write them by hand. The helper functions below are entirely optional — they provide TypeScript type safety, sensible default messages, and a more compact syntax. Every helper returns the same JSON shape you would write manually.
```ts
import { actionBinding, check, visibility } from "@prototyperco/compose/core";
```
## Action Helpers
The `actionBinding` object provides constructors for `ActionBinding` objects used in `on` event handlers and `watch` bindings.
### Reference
| Method | Signature | Description |
| ------------- | ------------------------------ | --------------------------------------------- |
| `simple` | `(action, params?)` | Simple action with optional params |
| `withConfirm` | `(action, confirm, params?)` | Action with confirmation dialog |
| `withSuccess` | `(action, onSuccess, params?)` | Action with success handler |
| `withError` | `(action, onError, params?)` | Action with error handler |
| `setState` | `(path, value)` | Set a state value at a path |
| `pushState` | `(path, value)` | Append a value to a state array |
| `removeState` | `(path, index)` | Remove an element from a state array by index |
| `toggleState` | `(path)` | Toggle a boolean at a state path |
| `navigate` | `(url)` | Navigate to a URL |
### Simple action
The most common case — fire a named action, optionally passing params:
```ts
actionBinding.simple("submit");
// → { action: "submit" }
actionBinding.simple("submit", { id: 1 });
// → { action: "submit", params: { id: 1 } }
```
### With confirmation
Show a confirmation dialog before executing the action. The `confirm` object accepts `title`, `message`, `confirmLabel`, and `cancelLabel`:
```ts
actionBinding.withConfirm("delete", {
title: "Delete?",
message: "This cannot be undone.",
confirmLabel: "Delete",
cancelLabel: "Keep",
});
// → { action: "delete", confirm: { title: "Delete?", message: "This cannot be undone.", ... } }
```
### With success or error handler
Chain a follow-up action after the primary action succeeds or fails:
```ts
actionBinding.withSuccess("save", { navigate: "/dashboard" });
// → { action: "save", onSuccess: { navigate: "/dashboard" } }
actionBinding.withError("save", { set: { "/error": "Save failed" } });
// → { action: "save", onError: { set: { "/error": "Save failed" } } }
```
### Built-in action shorthands
The most common state mutations have dedicated helpers that produce the correct `action` and `params` shape automatically.
**`setState`** — Set a value at a state path:
```ts
// Before (manual)
{ action: "setState", params: { path: "/count", value: 0 } }
// After (helper)
actionBinding.setState("/count", 0)
```
**`pushState`** — Append to a state array:
```ts
// Before
{ action: "pushState", params: { path: "/items", value: { name: "New item" } } }
// After
actionBinding.pushState("/items", { name: "New item" })
```
**`removeState`** — Remove from a state array by index:
```ts
// Before
{ action: "removeState", params: { path: "/items", index: 2 } }
// After
actionBinding.removeState("/items", 2)
```
**`toggleState`** — Toggle a boolean:
```ts
// Before
{ action: "toggleState", params: { path: "/sidebar/open" } }
// After
actionBinding.toggleState("/sidebar/open")
```
**`navigate`** — Navigate to a URL:
```ts
// Before
{ action: "navigate", params: { url: "/dashboard" } }
// After
actionBinding.navigate("/dashboard")
```
## Validation Helpers
The `check` object provides constructors for `ValidationCheck` objects. Each helper returns a check with a sensible default error message that you can override with the optional `message` parameter.
### Reference
| Method | Signature | Default message |
| ------------- | ----------------------- | ------------------------------------- |
| `required` | `(message?)` | `"This field is required"` |
| `email` | `(message?)` | `"Invalid email address"` |
| `numeric` | `(message?)` | `"Must be a number"` |
| `url` | `(message?)` | `"Invalid URL"` |
| `minLength` | `(min, message?)` | `"Must be at least {min} characters"` |
| `maxLength` | `(max, message?)` | `"Must be at most {max} characters"` |
| `min` | `(min, message?)` | `"Must be at least {min}"` |
| `max` | `(max, message?)` | `"Must be at most {max}"` |
| `pattern` | `(pattern, message?)` | `"Invalid format"` |
| `matches` | `(statePath, message?)` | `"Fields must match"` |
| `equalTo` | `(value, message?)` | `"Values must be equal"` |
| `lessThan` | `(statePath, message?)` | `"Must be less than reference"` |
| `greaterThan` | `(statePath, message?)` | `"Must be greater than reference"` |
| `requiredIf` | `(fieldPath, message?)` | `"This field is required"` |
### Basic validators
No-argument validators that check value format:
```ts
check.required();
// → { validator: "required", message: "This field is required" }
check.email();
// → { validator: "email", message: "Invalid email address" }
check.numeric();
// → { validator: "numeric", message: "Must be a number" }
check.url();
// → { validator: "url", message: "Invalid URL" }
```
### Length and range
Validators that accept a numeric threshold:
```ts
check.minLength(3);
// → { validator: "minLength", message: "Must be at least 3 characters", args: { min: 3 } }
check.maxLength(100);
// → { validator: "maxLength", message: "Must be at most 100 characters", args: { max: 100 } }
check.min(0);
// → { validator: "min", message: "Must be at least 0", args: { min: 0 } }
check.max(999);
// → { validator: "max", message: "Must be at most 999", args: { max: 999 } }
```
### Pattern
Match a value against a regular expression:
```ts
check.pattern("^[A-Z]");
// → { validator: "pattern", message: "Invalid format", args: { pattern: "^[A-Z]" } }
```
### Cross-field validation
Compare a value against another field in the state model. These helpers automatically wrap the path in a `{ $state }` reference:
```ts
check.matches("/password");
// → { validator: "matches", message: "Fields must match", args: { path: { $state: "/password" } } }
check.lessThan("/max");
// → { validator: "lessThan", message: "Must be less than reference", args: { path: { $state: "/max" } } }
check.greaterThan("/min");
// → { validator: "greaterThan", message: "Must be greater than reference", args: { path: { $state: "/min" } } }
check.equalTo(true);
// → { validator: "equalTo", message: "Values must be equal", args: { value: true } }
```
### Conditional requirement
Require a field only when another field is truthy:
```ts
check.requiredIf("/otherField");
// → { validator: "requiredIf", message: "This field is required", args: { path: { $state: "/otherField" } } }
```
### Dynamic args
The `min` and `max` helpers accept a `{ $state }` reference instead of a literal number, allowing the threshold to be read from state at validation time:
```ts
check.min({ $state: "/settings/minPrice" });
// → { validator: "min", message: "Must be at least the minimum", args: { min: { $state: "/settings/minPrice" } } }
check.max({ $state: "/settings/maxPrice" });
// → { validator: "max", message: "Must be at most the maximum", args: { max: { $state: "/settings/maxPrice" } } }
```
### Custom messages
Every helper accepts an optional message override as its last argument:
```ts
check.required("Please enter your name");
// → { validator: "required", message: "Please enter your name" }
check.minLength(8, "Password must be at least 8 characters");
// → { validator: "minLength", message: "Password must be at least 8 characters", args: { min: 8 } }
```
## Visibility Helpers
The `visibility` object provides constructors for `VisibilityCondition` objects used in the `visible` property of elements and the `enabled` property of validation checks.
### Constants
```ts
visibility.always; // → true
visibility.never; // → false
```
### State conditions
Check values in the global state model by JSON Pointer path:
```ts
visibility.when("/user/isLoggedIn");
// → { $state: "/user/isLoggedIn" } (truthy check)
visibility.unless("/ui/isLoading");
// → { $state: "/ui/isLoading", not: true } (falsy check)
visibility.eq("/user/role", "admin");
// → { $state: "/user/role", eq: "admin" }
visibility.neq("/status", "archived");
// → { $state: "/status", neq: "archived" }
visibility.gt("/cart/total", 100);
// → { $state: "/cart/total", gt: 100 }
visibility.gte("/age", 18);
// → { $state: "/age", gte: 18 }
visibility.lt("/inventory", 5);
// → { $state: "/inventory", lt: 5 }
visibility.lte("/score", 50);
// → { $state: "/score", lte: 50 }
```
### Dynamic comparisons
Numeric comparison helpers (`gt`, `gte`, `lt`, `lte`) accept a `{ $state }` reference to compare against another state value instead of a literal number:
```ts
visibility.gt("/price", { $state: "/budget" });
// → { $state: "/price", gt: { $state: "/budget" } }
visibility.lte("/currentStep", { $state: "/maxStep" });
// → { $state: "/currentStep", lte: { $state: "/maxStep" } }
```
### Logical combinators
Combine multiple conditions with AND or OR:
```ts
visibility.and(
visibility.when("/user/isLoggedIn"),
visibility.eq("/user/role", "admin"),
);
// → { $and: [{ $state: "/user/isLoggedIn" }, { $state: "/user/role", eq: "admin" }] }
visibility.or(
visibility.eq("/status", "active"),
visibility.eq("/status", "pending"),
);
// → { $or: [{ $state: "/status", eq: "active" }, { $state: "/status", eq: "pending" }] }
```
### Repeat item conditions
Inside a `repeat`, use `visibility.item` to check fields on the current repeat item:
```ts
visibility.item.when("completed");
// → { $item: "completed" } (truthy check on item.completed)
visibility.item.unless("deleted");
// → { $item: "deleted", not: true }
visibility.item.eq("status", "active");
// → { $item: "status", eq: "active" }
visibility.item.neq("status", "archived");
// → { $item: "status", neq: "archived" }
visibility.item.gt("price", 50);
// → { $item: "price", gt: 50 }
```
The `item` sub-object supports the same operators as state conditions: `when`, `unless`, `eq`, `neq`, `gt`, `gte`, `lt`, `lte`.
### Repeat index conditions
Inside a `repeat`, use `visibility.index` to check the current zero-based index:
```ts
visibility.index.eq(0);
// → { $index: true, eq: 0 } (first item only)
visibility.index.lt(3);
// → { $index: true, lt: 3 } (first three items)
visibility.index.neq(0);
// → { $index: true, neq: 0 } (skip first item)
```
The `index` sub-object supports: `eq`, `neq`, `gt`, `gte`, `lt`, `lte`.
### Combining conditions
A more complex example showing nested combinators with item and state conditions:
```ts
// Show the "remove" button only for non-first completed items when editing is enabled
visibility.and(
visibility.when("/editing"),
visibility.index.neq(0),
visibility.item.eq("status", "completed"),
);
```
## Complete Example
A registration form spec built entirely with helpers:
```ts
import { actionBinding, check, visibility } from "@prototyperco/compose/core";
import type { Spec } from "@prototyperco/compose/core";
const spec: Spec = {
root: "form",
elements: {
form: {
type: "Card",
props: { title: "Register" },
children: [
"email",
"pass",
"confirm",
"accountType",
"company",
"submit",
],
},
email: {
type: "Input",
props: {
label: "Email",
value: { $bindState: "/email" },
validation: {
checks: [check.required(), check.email()],
validateOn: "blur",
},
},
},
pass: {
type: "Input",
props: {
label: "Password",
type: "password",
value: { $bindState: "/password" },
validation: {
checks: [
check.required(),
check.minLength(8),
check.pattern("\\d", "Must contain a number"),
],
validateOn: "change",
},
},
},
confirm: {
type: "Input",
props: {
label: "Confirm Password",
type: "password",
value: { $bindState: "/confirmPassword" },
validation: {
checks: [
check.required("Please confirm your password"),
check.matches("/password"),
],
validateOn: "change",
},
},
},
accountType: {
type: "Select",
props: {
label: "Account Type",
value: { $bindState: "/accountType" },
options: ["personal", "business"],
},
},
company: {
type: "Input",
props: {
label: "Company Name",
value: { $bindState: "/company" },
validation: {
checks: [check.requiredIf("/accountType")],
validateOn: "blur",
},
},
visible: visibility.eq("/accountType", "business"),
},
submit: {
type: "Button",
props: { label: "Register" },
on: {
press: actionBinding.simple("register"),
},
},
},
state: {
email: "",
password: "",
confirmPassword: "",
accountType: "personal",
company: "",
},
};
```
Compare this to the equivalent raw JSON in the [Validation](/docs/compose/validation#complete-form-example) page — the helper version is more compact, fully type-checked, and the default messages are generated automatically.
# MCP Integration
> Connect AI assistants to Compose via Model Context Protocol
URL: https://prototyper-ui.com/docs/compose/mcp
The [Model Context Protocol](https://modelcontextprotocol.io/) (MCP) is an open standard that lets AI assistants connect to external tools and data sources. The Prototyper UI MCP server exposes Compose's component catalog and prompt generation as tools that any MCP-compatible client (Claude, Cursor, etc.) can use.
## Available Tools
The MCP server provides 7 tools. Two are specific to Compose:
### get_ui_catalog
Returns the Compose component catalog as JSON Schema. This includes all available components with their props, events, slots, and descriptions.
The output is the same as calling `catalog.jsonSchema()` — a machine-readable schema that AI models can use to understand what components are available and how to construct valid specs.
### get_ui_prompt
Returns the pre-built system prompt generated by `buildSystemPrompt(catalog)`. This is a complete instruction set that teaches the AI model:
- The JSONL streaming output format (RFC 6902 JSON Patch)
- The flat spec structure
- All dynamic expressions (`$state`, `$bindState`, `$cond`, etc.)
- Every available component with prop types and examples
- Built-in and custom actions
- Rules for valid spec generation
### Live Canvas Tools
The MCP server also includes 7 tools for the [Live Canvas](/design) — a live AI-to-browser design system that pushes spec and theme changes over WebSocket:
| Tool | Description |
| --------------- | ------------------------------------------------- |
| `design_create` | Create a new live design session with preview URL |
| `design_update` | Push spec changes to the browser in real time |
| `design_theme` | Update theme tokens (hue, chroma, radius, font) |
| `design_get` | Get current session state (spec, theme, revision) |
| `design_list` | List all active design sessions |
| `design_close` | Close a design session |
| `design_export` | Export session as standalone HTML or Compose spec |
In normal agent flows, the bridge auto-starts on the first `design_create` when the local bridge package is available. For local docs development you can still run `pnpm dev`, which serves the docs on port 3333 and the bridge on 4321.
### Other Tools
The server also includes general Prototyper UI tools:
| Tool | Description |
| --------------------- | ----------------------------------------------------------------------- |
| `list_components` | List all available Prototyper UI components by category |
| `get_component` | Get full docs, source code, and examples for components (batch up to 5) |
| `get_theme` | Get CSS design tokens (OKLCH colors, surfaces, shadows, easings) |
| `search_docs` | Full-text search across all documentation |
| `get_install_command` | Get the CLI install command for components |
## Setup
Add the MCP server to your AI assistant:
```bash
claude mcp add prototyper-ui -- npx -y @prototyperco/mcp@latest
```
Add to your `claude_desktop_config.json`:
```json
{
"mcpServers": {
"prototyper-ui": {
"command": "npx",
"args": ["-y", "@prototyperco/mcp@latest"]
}
}
}
```
### Custom Base URL
By default, the MCP server fetches from `https://prototyper-ui.com`. To point it at a local dev server or custom deployment, set the `PROTOTYPER_UI_BASE_URL` environment variable:
```bash
PROTOTYPER_UI_BASE_URL=http://localhost:3333 npx -y @prototyperco/mcp@latest
```
## Workflow
A typical AI-assisted UI generation flow using MCP:
### Discover Components
The AI calls `get_ui_catalog` to receive the full component catalog as JSON Schema. This tells it what components exist, what props they accept, and what events they emit.
### Get the System Prompt
The AI calls `get_ui_prompt` to receive the system prompt. This prompt contains the complete spec format documentation, streaming instructions, and all the rules for generating valid specs.
### Generate the Spec
Using the catalog knowledge and system prompt, the AI generates a streaming spec (JSONL patches) that builds a UI matching the user's request.
### Render with Compose
The generated spec is passed to `` with the matching component registry. The UI renders progressively as patches stream in.
## Resources
The MCP server also exposes resources that clients can read directly:
| Resource | URI | Description |
| --------------- | ------------------------------------- | ----------------------------------------------- |
| Design tokens | `prototyper://tokens/css` | Full CSS design tokens file |
| Component index | `prototyper://docs/index` | Index of all components and documentation pages |
| Compose catalog | `prototyper://compose/catalog` | Component catalog as JSON Schema |
| Component docs | `prototyper://docs/components/{name}` | Per-component documentation (dynamic) |
## Prompts
The server includes two built-in prompt templates:
| Prompt | Description |
| -------------- | -------------------------------------------------------------- |
| `build_ui` | Generate a UI component or page using Prototyper UI components |
| `review_usage` | Review code for correct Prototyper UI component usage |
These can be invoked directly by MCP-compatible clients that support the prompts capability.
## Direct API Access
If you are not using MCP, you can access the same data through HTTP endpoints:
| Endpoint | Content |
| ------------------------ | -------------------------------- |
| `/compose/catalog.json` | Component catalog as JSON Schema |
| `/compose/prompt.txt` | Pre-built system prompt |
| `/llms.txt` | Component index for LLMs |
| `/llms-full.txt` | Full documentation for LLMs |
| `/llms/{component}` | Per-component documentation |
| `/prototyper-tokens.css` | Extractable CSS design tokens |
These endpoints return the same data that the MCP tools serve, making it straightforward to integrate with any AI pipeline.
# Prompt Engineering
> Customize AI prompts, use structured outputs, and optimize generation quality.
URL: https://prototyper-ui.com/docs/compose/prompts
The prompt system converts your catalog into precise instructions for AI models. `buildSystemPrompt` generates the system message describing your components, spec format, and rules. `buildUserPrompt` wraps user requests with context for fresh generation or refinement.
## Prompt Modes
The `mode` option controls how the AI formats its output:
````ts
import { buildSystemPrompt } from "@prototyperco/compose/catalog";
// Generate mode (default): AI outputs ONLY JSONL patches, no prose
const generatePrompt = buildSystemPrompt(catalog, { mode: "generate" });
// Chat mode: AI responds conversationally, wraps JSONL in ```spec fences
const chatPrompt = buildSystemPrompt(catalog, { mode: "chat" });
````
**Generate mode** (`"generate"`, default) tells the model to output only valid JSONL patches — one JSON object per line, no explanations, no markdown, no code fences. Use this when piping output directly into a `StreamCompiler`.
**Chat mode** (`"chat"`) tells the model to respond naturally and wrap its JSONL patches inside ` ```spec ` code fences. The model can include explanatory text before and after the fence. Use this when building a conversational UI where the user sees both the AI's explanation and the rendered result.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
## Custom Rules
Append domain-specific rules to the system prompt with `customRules`. Each string becomes a bullet point in the RULES section the AI sees:
```ts
const prompt = buildSystemPrompt(catalog, {
customRules: [
"Always use the Card component as the root element",
"Prefer dark variant for all buttons",
"Include at least 3 sample items in any list",
],
});
```
Custom rules appear after the built-in rules (use only catalog components, always include a root, use flat keys, etc.), so they can refine or constrain the default behavior without overriding it.
## Custom System Introduction
Override the default system introduction with the `system` option. This replaces the opening paragraph that describes the AI's role:
```ts
const prompt = buildSystemPrompt(catalog, {
system:
"You are a dashboard builder. Generate admin interfaces using JSON Patch format. Focus on data tables and charts.",
});
```
The default introduction is:
> You are a UI generator that outputs JSON. You generate user interfaces by producing a flat UI spec in JSONL (streaming JSON Patch) format.
Everything after the introduction (output format, spec structure, component catalog, rules) is still generated automatically from your catalog.
## Custom Prompt Templates
For full control over the system prompt, pass a `template` function. It receives a `PromptContext` with the catalog, component names, and the default prompt as a starting point:
```ts
import type { PromptContext } from "@prototyperco/compose/catalog";
const prompt = buildSystemPrompt(catalog, {
template: (ctx: PromptContext) => {
// Start with the default prompt, then append custom sections
return `${ctx.defaultPrompt}
## BRAND GUIDELINES
- Use blue (#2563eb) as the primary action color
- All headings must use sentence case
- Maximum 3 levels of nesting
`;
},
});
```
The `PromptContext` object exposes:
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `catalog` | `Catalog` | - | The full catalog instance with components and actions |
| `componentNames` | `string[]` | - | All registered component type names |
| `actionNames` | `string[]` | - | All registered action names |
| `options` | `PromptOptions` | - | The options passed to buildSystemPrompt |
| `formatZodType` | `(schema: z.ZodType) => string` | - | Format a Zod schema into a human-readable string |
| `defaultPrompt` | `string` | - | The full default prompt (before template is applied) |
You can also build a prompt from scratch using `ctx.catalog` and `ctx.formatZodType`:
```ts
const prompt = buildSystemPrompt(catalog, {
template: (ctx) => {
const components = ctx.componentNames
.map((name) => {
const def = ctx.catalog.components[name]!;
return `- ${name}: ${ctx.formatZodType(def.props)}`;
})
.join("\n");
return `You are a form builder. Only use these components:\n${components}\n\nOutput JSONL patches.`;
},
});
```
## User Prompts and Refinement
`buildUserPrompt` wraps a user's request with context about the current spec and application state. It has two modes depending on whether you pass a `currentSpec`:
### Fresh Generation
When no `currentSpec` is provided, the prompt reminds the model to stream patches progressively:
```ts
import { buildUserPrompt } from "@prototyperco/compose/catalog";
const prompt = buildUserPrompt("Build a pricing page with three tiers");
```
### Refinement Mode
When you pass an existing spec, the prompt includes the full spec as JSON and instructs the model to output only the patches needed to make the requested change — not a full regeneration:
```ts
const prompt = buildUserPrompt("Add a free trial toggle to the header", {
currentSpec: existingSpec,
});
```
The model receives the current spec and instructions like:
- To add a new element: `{"op":"add","path":"/elements/new-key","value":{...}}`
- To modify an existing element: `{"op":"replace","path":"/elements/existing-key","value":{...}}`
- To remove an element: `{"op":"remove","path":"/elements/old-key"}`
This keeps refinement responses fast and focused.
### State Context
Provide application data so the model can generate data-driven UIs:
```ts
const prompt = buildUserPrompt("Show a table of these users", {
stateContext: {
users: [
{ id: 1, name: "Alice", role: "Admin" },
{ id: 2, name: "Bob", role: "Editor" },
],
currency: "USD",
},
});
```
The state context is included as a JSON block with instructions to reference it via `$state` expressions.
### Prompt Length Limits
Truncate user input to avoid exceeding model context limits:
```ts
const prompt = buildUserPrompt(veryLongUserInput, {
maxPromptLength: 2000, // Truncates the user's text to 2000 characters
});
```
### UserPromptOptions
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `currentSpec` | `Spec \| null` | - | When provided, enables refinement mode — model outputs only patches to modify the existing spec |
| `stateContext` | `Record` | - | Application data included as context for data-driven generation |
| `maxPromptLength` | `number` | - | Truncate the user's prompt text to this character length |
## Token Budgeting
The system prompt includes your entire component catalog — every component's name, props schema, description, events, and slots. For large catalogs, this can consume significant tokens.
**Strategies to reduce token usage:**
1. **Keep descriptions concise.** Each component's `description` field appears verbatim in the prompt.
2. **Use `example` props.** When provided, the prompt uses your example instead of auto-generating one from the schema.
3. **Split catalogs.** Create focused sub-catalogs for different use cases instead of one catalog with everything:
```ts
const formCatalog = defineCatalog({
components: { Input, Select, Checkbox, RadioGroup, Button },
});
const dashboardCatalog = defineCatalog({
components: { Card, Heading, Text, Table, Chart, Badge },
});
// Use the right catalog for the task
const prompt = buildSystemPrompt(formCatalog);
```
4. **Use a template to trim sections.** If you know the model already understands certain concepts, use a `template` to remove sections like the dynamic values reference or the repeat/list docs.
**Approximate token sizes** (varies by catalog):
- Base prompt (format, rules, examples): ~800 tokens
- Per component: ~30–80 tokens depending on schema complexity
- State/expressions/actions reference: ~400 tokens
## Structured Outputs vs Streaming
Compose supports two approaches for AI-generated UIs:
| | **JSONL Streaming** | **Structured Output (JSON Schema)** |
| ------------------------- | ------------------------------------------ | --------------------------------------------------------- |
| **Format** | Newline-delimited JSON Patch operations | Single JSON object matching the full spec schema |
| **Progressive rendering** | Yes — UI fills in as patches arrive | No — UI renders only after full response |
| **Refinement** | Patches modify existing spec incrementally | Full spec must be regenerated |
| **Model support** | Any model that outputs text | Models with structured output support (OpenAI, Anthropic) |
| **Validation** | Per-line — malformed lines are skipped | Schema-enforced by the model provider |
| **Token efficiency** | Lower for refinements (patches only) | Higher for refinements (full spec each time) |
| **Setup** | `buildSystemPrompt` + `StreamCompiler` | `catalog.jsonSchema()` + provider's structured output API |
**Use JSONL streaming** (the default) when you want progressive rendering, efficient refinement, and maximum model compatibility.
**Use structured outputs** when you need guaranteed schema compliance and don't need progressive rendering. Export the schema with:
```ts
const jsonSchema = catalog.jsonSchema();
// Pass to OpenAI's response_format, Anthropic's tool_use, etc.
```
## API Endpoints
The docs site exposes three endpoints for external tooling:
### GET /compose/prompt.txt
Returns the full system prompt as plain text. Supports query parameters:
| Parameter | Type | Description |
| --------- | ---------------------- | ----------------------------------- |
| `mode` | `"generate" \| "chat"` | Prompt mode (default: `"generate"`) |
| `rules` | `string` | Comma-separated custom rules |
```bash
# Default generate mode
curl https://prototyper-ui.com/compose/prompt.txt
# Chat mode with custom rules
curl "https://prototyper-ui.com/compose/prompt.txt?mode=chat&rules=Use%20dark%20theme,Max%203%20elements"
```
### GET /compose/schema.json
Returns the full JSON Schema for the spec format, derived from all catalog component and action Zod schemas:
```bash
curl https://prototyper-ui.com/compose/schema.json
```
Use this with OpenAI's `response_format: { type: "json_schema", json_schema: schema }` or similar structured output APIs.
### POST /compose/validate
Validates a spec against the catalog. Optionally auto-fixes common issues:
```bash
curl -X POST https://prototyper-ui.com/compose/validate \
-H "Content-Type: application/json" \
-d '{"spec": {"root": "main", "elements": {"main": {"type": "Card", "props": {}}}}, "autofix": true}'
```
**Request body:**
| Field | Type | Description |
| --------- | --------- | ----------------------------------------------------------------------------- |
| `spec` | `object` | The spec to validate |
| `autofix` | `boolean` | When `true` and spec is invalid, return a corrected spec with a list of fixes |
**Response:**
```json
{
"valid": true,
"issues": [],
"fixed": {
"spec": { "...": "..." },
"fixes": ["Added missing children array to element 'main'"]
}
}
```
The `fixed` field is only present when `autofix: true` and the spec had issues.
## Best Practices
**Start with the defaults.** The built-in prompt covers the spec format, all dynamic expressions, repeat/list rendering, visibility conditions, events, actions, and your full component catalog. Most use cases need only `buildSystemPrompt(catalog)`.
**Use refinement mode for edits.** When the user wants to modify an existing UI, always pass `currentSpec` to `buildUserPrompt`. This produces smaller, faster responses because the model outputs only the patches needed.
**Provide state context for data-driven UIs.** When your app has data the UI should display, pass it as `stateContext` so the model can reference it with `$state` expressions instead of inventing placeholder data.
**Test prompts with the API endpoints.** Fetch `/compose/prompt.txt` to see exactly what the model receives. This is the fastest way to debug generation issues — if the prompt is wrong, the output will be wrong.
**Validate after generation.** Use `catalog.validate(spec)` or the `/compose/validate` endpoint to catch issues in generated specs before rendering. The `autofix` option can correct common structural problems automatically.
**Prefer chat mode for user-facing conversations.** If your product shows the AI's response alongside the rendered UI, use `mode: "chat"` so the model can explain its changes. If the AI's response goes directly to a renderer, use `mode: "generate"`.
## Next Steps
- [Component Catalog](/docs/compose/catalog) — Define components and actions for the prompt system
- [Streaming](/docs/compose/streaming) — Connect prompts to the streaming pipeline
- [Validation](/docs/compose/validation) — Validate generated specs against your catalog
- [API Reference](/docs/compose/api-reference) — Full API reference for all exports
# Spec Format
> The flat JSON structure that describes a Compose interface
URL: https://prototyper-ui.com/docs/compose/spec-format
A **spec** is the JSON document that describes an entire UI. Compose uses a deliberately flat structure optimized for streaming and efficient patching.
## The Spec Type
Every spec has three fields:
```ts
interface Spec {
/** Key of the root element in the elements map. */
root: string;
/** Flat map of element keys to element definitions. */
elements: Record;
/** Optional initial state model. */
state?: Record;
}
```
| Field | Required | Description |
| ---------- | -------- | ------------------------------------------------------------------- |
| `root` | Yes | The key in `elements` that serves as the tree root. |
| `elements` | Yes | A flat dictionary of all UI elements, keyed by unique string IDs. |
| `state` | No | Initial state values, readable and writable via JSON Pointer paths. |
## UIElement Fields
Each element in the `elements` map is a `UIElement`:
```ts
interface UIElement {
/** Component type from the registry (e.g. "Button", "Card", "Input"). */
type: string;
/** Component props — may contain dynamic expressions. */
props: Record;
/** Ordered array of child element keys. */
children?: string[];
/** Visibility condition — controls conditional rendering. */
visible?: VisibilityCondition;
/** Event bindings — maps event names to action bindings. */
on?: Record;
/** Repeat over items in a state array. */
repeat?: RepeatBinding;
/** State paths to watch for triggering side effects. */
watch?: string[];
}
```
| Field | Required | Description |
| ---------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| `type` | Yes | Component type name matching a key in the component registry. |
| `props` | Yes | Props object passed to the component. Values can be literals or [expressions](/docs/compose/expressions). |
| `children` | No | Array of element keys rendered as children, in order. |
| `visible` | No | A [visibility condition](/docs/compose/visibility) controlling whether this element renders. Defaults to `true`. |
| `on` | No | Event-to-[action](/docs/compose/actions) map. Keys are event names (`press`, `change`, `blur`), values are `ActionBinding` objects. |
| `repeat` | No | Binds the element to iterate over items in a state array. Children are rendered once per item. |
| `watch` | No | Array of state paths. When a watched value changes, the `watch:` event fires. |
## Why Flat (Not Nested)?
Compose uses a flat element map instead of a nested tree for three key reasons:
### 1. Streaming-Optimized
A flat structure lets the model output elements in any order. Each JSON Patch targets a specific element by key:
```json
{
"op": "add",
"path": "/elements/email-input",
"value": { "type": "Input", "props": { "label": "Email" } }
}
```
With nested trees, adding a deeply nested element requires the model to output the full path through all ancestors.
### 2. Efficient Patching
Updating a single element only requires a patch to `/elements/`. Siblings and parents are untouched. React re-renders only the affected subtree.
### 3. Independent Updates
Elements can be added, removed, or modified independently. An element's children are references (string keys), not inline definitions, so restructuring the tree only requires changing `children` arrays.
## Element Keys
Keys are semantic string identifiers. Use descriptive names that reflect the element's purpose:
```json
{
"root": "login-card",
"elements": {
"login-card": {
"type": "Card",
"props": {},
"children": ["heading", "form-fields", "submit-btn"]
},
"heading": {
"type": "Heading",
"props": { "text": "Sign In", "level": 2 }
},
"form-fields": {
"type": "Card",
"props": {},
"children": ["email-input", "password-input"]
},
"email-input": {
"type": "Input",
"props": { "label": "Email", "type": "email" }
},
"password-input": {
"type": "Input",
"props": { "label": "Password", "type": "password" }
},
"submit-btn": { "type": "Button", "props": { "label": "Sign In" } }
}
}
```
Conventions:
- Use lowercase kebab-case: `email-input`, `submit-btn`, `login-card`
- Name elements by role, not type: `heading` not `heading-1`, `submit-btn` not `button-3`
- Keep keys short but unambiguous within the spec
## Repeat Binding
The `repeat` field binds an element to iterate over a state array:
```ts
interface RepeatBinding {
/** JSON Pointer path to a state array in the state model. */
source: string;
/** Field on each item to use as the React key (optional). */
itemKey?: string;
}
```
When `repeat` is set, the element's `children` are rendered once for each item in the source array. Inside the repeat scope, child elements can use `$item` and `$index` expressions.
```json
{
"root": "list",
"elements": {
"list": {
"type": "Card",
"props": {},
"children": ["item-text"],
"repeat": { "source": "/todos", "itemKey": "id" }
},
"item-text": {
"type": "Text",
"props": { "content": { "$item": "title" } }
}
},
"state": {
"todos": [
{ "id": "1", "title": "Buy groceries" },
{ "id": "2", "title": "Walk the dog" }
]
}
}
```
## The `nestedToFlat()` Utility
If you prefer writing nested trees for convenience, the `nestedToFlat()` function converts them to the flat format:
```tsx
import { nestedToFlat } from "@prototyperco/compose/core";
const spec = nestedToFlat({
state: { name: "World" },
root: {
type: "Card",
props: {},
children: [
{ type: "Heading", props: { text: "Hello", level: 2 } },
{ type: "Text", props: { content: "Welcome" } },
{
type: "Button",
props: { label: "Click" },
on: {
press: {
action: "setState",
params: { path: "/clicked", value: true },
},
},
},
],
},
});
```
The converter auto-generates element keys (`el-0`, `el-1`, ...) and preserves all fields including `visible`, `on`, `repeat`, and `watch`.
## Full Annotated Example
Here is a complete spec for a contact form with validation and conditional state:
```json
{
"root": "form-card",
"elements": {
"form-card": {
"type": "Card",
"props": {},
"children": [
"heading",
"name-input",
"email-input",
"message-input",
"submit-btn",
"success-msg"
]
},
"heading": {
"type": "Heading",
"props": { "text": "Contact Us", "level": 2 }
},
"name-input": {
"type": "Input",
"props": {
"label": "Name",
"placeholder": "Your name",
"value": { "$bindState": "/form/name" }
}
},
"email-input": {
"type": "Input",
"props": {
"label": "Email",
"type": "email",
"placeholder": "you@example.com",
"value": { "$bindState": "/form/email" }
}
},
"message-input": {
"type": "Textarea",
"props": {
"label": "Message",
"placeholder": "How can we help?",
"value": { "$bindState": "/form/message" }
}
},
"submit-btn": {
"type": "Button",
"props": { "label": "Send Message" },
"on": {
"press": {
"action": "setState",
"params": { "path": "/submitted", "value": true }
}
},
"visible": { "$state": "/submitted", "not": true }
},
"success-msg": {
"type": "Alert",
"props": {
"title": "Message sent!",
"description": {
"$template": "Thanks ${/form/name}, we'll get back to you at ${/form/email}."
}
},
"visible": { "$state": "/submitted" }
}
},
"state": {
"form": { "name": "", "email": "", "message": "" },
"submitted": false
}
}
```
This example demonstrates:
- **Two-way binding** with `$bindState` on form inputs
- **Visibility conditions** to toggle between the form and success message
- **Template expressions** for dynamic text interpolation
- **Action binding** on the submit button
## Next Steps
- [Expressions](/docs/compose/expressions) — All dynamic value types
- [Visibility](/docs/compose/visibility) — Conditional rendering in depth
- [Actions](/docs/compose/actions) — Event handling and state mutations
- [Streaming](/docs/compose/streaming) — How patches build specs incrementally
# Store Adapters
> Integrate Compose with external state managers like Zustand, Redux, or Jotai.
URL: https://prototyper-ui.com/docs/compose/store-adapters
By default, Compose uses its own lightweight reactive store (`createStateStore`) to manage spec state. This works well for self-contained UIs, but when your app already has a state management layer, you may want compose to read from and write to that same store. Store adapters bridge the gap: they wrap an external store so it conforms to the `StateStore` interface that the engine expects.
## The StateStore Interface
Every store in compose, whether built-in or adapted, implements this interface:
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
The built-in `createStateStore` holds an immutable snapshot in memory, uses structural sharing on writes, and notifies listeners only when the root reference changes. `createStoreAdapter` produces an object with the same shape, but delegates to your external store for storage and subscription.
## createStoreAdapter
```ts
import { createStoreAdapter } from "@prototyperco/compose/core"
import type { StoreAdapterConfig } from "@prototyperco/compose/core"
const adapter = createStoreAdapter({
getSnapshot: () => /* return current state object */,
setSnapshot: (next) => /* replace state with next */,
subscribe: (listener) => /* register listener, return unsubscribe */,
})
```
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `getSnapshot` | `() => StateModel` | - | Return the current state snapshot from your external store |
| `setSnapshot` | `(state: StateModel) => void` | - | Replace the entire state snapshot in your external store |
| `subscribe` | `(listener: () => void) => () => void` | - | Subscribe to state changes. Must return an unsubscribe function |
The adapter handles JSON Pointer path resolution, immutable updates with structural sharing, and batched multi-path writes internally. Your config only needs to provide raw get/set/subscribe for the full state object.
### How It Works
When the engine calls `adapter.set("/form/email", "a@b.com")`:
1. `getSnapshot()` retrieves the current state from your store.
2. `immutableSetByPath` produces a new state object with structural sharing (only the changed branch is cloned).
3. If the value actually changed, `setSnapshot(next)` pushes the new state into your store.
4. Your store's own subscription mechanism notifies listeners, including the compose renderer.
The `update()` method works the same way but batches multiple path writes into a single `setSnapshot` call.
## Wiring the Adapter
The `ComposeProvider` currently creates an internal `createStateStore` automatically. To use a store adapter, compose the lower-level providers directly and pass your adapter to `StateProvider`:
```tsx
import {
StateProvider,
ActionProvider,
FunctionsProvider,
} from "@prototyperco/compose";
import { ElementRenderer } from "@prototyperco/compose";
function CustomRenderer({ spec, registry, storeAdapter, handlers }) {
return (
);
}
```
The `StateProvider` accepts any object implementing the `StateStore` interface, which is exactly what `createStoreAdapter` returns.
## Zustand Example
[Zustand](https://github.com/pmndrs/zustand) stores expose `getState`, `setState`, and `subscribe` directly, making the adapter setup straightforward.
```ts
import { create } from "zustand";
import { createStoreAdapter } from "@prototyperco/compose/core";
const useAppStore = create(() => ({
form: { name: "", email: "" },
theme: "light",
items: [],
}));
const storeAdapter = createStoreAdapter({
getSnapshot: () => useAppStore.getState(),
setSnapshot: (next) => useAppStore.setState(next, true), // true = replace (not merge)
subscribe: (listener) => useAppStore.subscribe(listener),
});
```
Pass `true` as the second argument to `setState` so the state is replaced rather than shallow-merged. The adapter already handles immutable updates with structural sharing, so a full replacement is correct.
Now Zustand and compose share the same state. Reading `/form/email` in a spec expression reads from the Zustand store, and writing via `$bindState` updates it. You can also read the state in your own components with the normal Zustand hook:
```tsx
function Header() {
const theme = useAppStore((s) => s.theme);
return (
);
}
```
## Redux Toolkit Example
Redux stores have a similar shape. The key difference is that `getState()` returns the full Redux state tree, so you may want to scope the adapter to a specific slice.
```ts
import { configureStore, createSlice } from "@reduxjs/toolkit";
import { createStoreAdapter } from "@prototyperco/compose/core";
const uiSlice = createSlice({
name: "ui",
initialState: {
form: { name: "", email: "" },
step: 0,
},
reducers: {
replaceState: (_state, action) => action.payload,
},
});
const reduxStore = configureStore({
reducer: { ui: uiSlice.reducer },
});
const storeAdapter = createStoreAdapter({
getSnapshot: () => reduxStore.getState().ui,
setSnapshot: (next) =>
reduxStore.dispatch(uiSlice.actions.replaceState(next)),
subscribe: (listener) => reduxStore.subscribe(listener),
});
```
The adapter reads and writes only the `ui` slice. Other Redux slices remain untouched. Because `subscribe` fires on any Redux state change, the adapter's internal `===` check on the snapshot prevents unnecessary re-renders when unrelated slices update.
## Jotai Example
Jotai is atom-based, so the adapter wraps a single atom that holds the full state object.
```ts
import { createStore, atom } from "jotai";
import { createStoreAdapter } from "@prototyperco/compose/core";
const stateAtom = atom({
form: { name: "", email: "" },
count: 0,
});
const jotaiStore = createStore();
const storeAdapter = createStoreAdapter({
getSnapshot: () => jotaiStore.get(stateAtom),
setSnapshot: (next) => jotaiStore.set(stateAtom, next),
subscribe: (listener) => jotaiStore.sub(stateAtom, listener),
});
```
Jotai's `createStore()` provides an imperative API (`get`, `set`, `sub`) that maps directly to the adapter config. The `sub` method returns an unsubscribe function, matching the expected signature.
## When to Use a Store Adapter
**Use an adapter when:**
- Your app already has a Zustand/Redux/Jotai store and you want compose state to live alongside app state in a single source of truth.
- You need to read compose state from components outside the `Renderer` tree (e.g., a global header that reacts to form progress).
- You want to persist state to localStorage, sync it across tabs, or apply middleware (logging, undo/redo) through your existing store's ecosystem.
- Multiple `Renderer` instances need to share the same state.
**Stick with the built-in store when:**
- The compose component is self-contained and doesn't need to share state with the rest of your app.
- You want the simplest setup with no external dependencies.
- The spec's `state` field fully describes the initial state and nothing outside the renderer needs to read or write it.
## Tips
- **Structural sharing is handled for you.** The adapter uses `immutableSetByPath` internally, so even if your external store does reference-equality checks (like Zustand selectors), only changed branches get new references.
- **Batch writes are atomic.** When the engine calls `update({ "/a": 1, "/b": 2 })`, your store receives a single `setSnapshot` call with both changes applied.
- **No-op writes are skipped.** If `set()` is called with a value identical to what's already at that path, `setSnapshot` is never called, avoiding unnecessary re-renders.
- **Initialize your external store with the spec's initial state.** The spec's `state` field won't be applied automatically when using an adapter — your store should already contain the matching initial values.
# Streaming
> Connect Compose to an AI model with JSONL streaming and progressive rendering
URL: https://prototyper-ui.com/docs/compose/streaming
Compose renders interfaces progressively as an AI model generates them. The model outputs JSONL (newline-delimited JSON), where each line is an RFC 6902 JSON Patch operation that incrementally builds the spec.
## JSONL Format
Each line in the stream is a single JSON Patch operation:
```json
{"op":"add","path":"/root","value":"card"}
{"op":"add","path":"/elements/card","value":{"type":"Card","props":{},"children":["heading"]}}
{"op":"add","path":"/elements/heading","value":{"type":"Heading","props":{"text":"Hello","level":2}}}
{"op":"add","path":"/state","value":{"count":0}}
```
The stream compiler processes these lines incrementally. As each valid line arrives, it applies the patch to the in-progress spec and emits an updated snapshot for React to render.
## Patch Operations
Compose supports all RFC 6902 operations:
| Operation | Required Fields | Description |
| --------- | --------------- | ----------------------------------------------------------------------- |
| `add` | `path`, `value` | Insert a new value at the path. Creates intermediate objects as needed. |
| `replace` | `path`, `value` | Overwrite the existing value at the path. |
| `remove` | `path` | Delete the value at the path. |
| `move` | `path`, `from` | Remove the value at `from` and add it at `path`. |
| `copy` | `path`, `from` | Copy the value at `from` to `path`. |
| `test` | `path`, `value` | Assert the value at `path` equals `value`. Throws on mismatch. |
### Common Patterns
**Build the initial structure:**
```json
{"op":"add","path":"/root","value":"main"}
{"op":"add","path":"/elements/main","value":{"type":"Card","props":{},"children":[]}}
{"op":"add","path":"/state","value":{}}
```
**Add elements incrementally:**
```json
{"op":"add","path":"/elements/title","value":{"type":"Heading","props":{"text":"Dashboard","level":1}}}
{"op":"add","path":"/elements/main/children/-","value":"title"}
```
The `/-` path suffix appends to an array (RFC 6902 array append).
**Update a prop on an existing element:**
```json
{
"op": "replace",
"path": "/elements/title/props/text",
"value": "Updated Title"
}
```
**Remove an element:**
```json
{ "op": "remove", "path": "/elements/old-widget" }
```
## Progressive Streaming Pattern
A typical stream follows this order:
### Root and scaffold
The model outputs the root key and top-level container elements first. The UI shows the basic structure immediately.
```json
{"op":"add","path":"/root","value":"page"}
{"op":"add","path":"/elements/page","value":{"type":"Card","props":{},"children":[]}}
```
### Elements
Individual elements are added one by one. Each `add` to an element, followed by appending its key to its parent's `children` array, makes it visible immediately.
```json
{"op":"add","path":"/elements/heading","value":{"type":"Heading","props":{"text":"Welcome","level":2}}}
{"op":"add","path":"/elements/page/children/-","value":"heading"}
{"op":"add","path":"/elements/intro","value":{"type":"Text","props":{"content":"Hello world"}}}
{"op":"add","path":"/elements/page/children/-","value":"intro"}
```
### State
State is typically added last (or alongside the elements that use it), since elements can render without state — expressions just resolve to `undefined` until state arrives.
```json
{"op":"add","path":"/state/user","value":{"name":"Alice","email":"alice@example.com"}}
{"op":"add","path":"/state/preferences","value":{"theme":"dark"}}
```
## `useUIStream` Hook
The `useUIStream` hook manages the entire streaming lifecycle: connecting to an endpoint, feeding chunks to the stream compiler, and exposing the latest spec for rendering.
```tsx
"use client";
import { Renderer, useUIStream } from "@prototyperco/compose";
import { prototyperComponents } from "@prototyperco/compose/components";
export function StreamingUI() {
const { spec, isStreaming, error, send, clear } = useUIStream({
url: "/api/compose",
});
return (
send({ prompt: "Build a settings page" })}>
Generate
Clear
{error &&
Error: {error.message}
}
{spec && (
)}
);
}
```
### Options
| Option | Type | Default | Description |
| ----------- | ------------------------ | ----------- | -------------------------------------------------------------------------------- |
| `url` | `string` | Required | Endpoint URL that returns a JSONL stream. |
| `method` | `"GET" \| "POST"` | `"POST"` | HTTP method for the request. |
| `headers` | `Record` | `{}` | Additional request headers. `Content-Type: application/json` is always included. |
| `body` | `unknown` | `undefined` | Default request body (overridden by `send(body)`). |
| `autoStart` | `boolean` | `false` | If `true`, starts streaming immediately on mount. |
### Return Value
| Field | Type | Description |
| ------------- | -------------------------- | --------------------------------------------------------------------------------------------------------------------- |
| `spec` | `Spec \| null` | The latest compiled spec, or `null` before any data arrives. |
| `isStreaming` | `boolean` | Whether a stream is currently in progress. |
| `error` | `Error \| null` | The last error, or `null`. Cleared on each new `send()`. |
| `send` | `(body?: unknown) => void` | Start a new stream. Aborts any in-progress stream. The optional `body` parameter overrides the default `body` option. |
| `clear` | `() => void` | Abort any in-progress stream, reset the spec to `null`, and clear errors. |
## `createStreamCompiler` for Custom Integrations
If you need lower-level control (e.g. integrating with a WebSocket, Server-Sent Events, or a custom transport), use `createStreamCompiler` directly:
```tsx
import { createStreamCompiler } from "@prototyperco/compose/core";
const compiler = createStreamCompiler();
// Push arbitrary text chunks — the compiler handles partial lines
const updatedSpec = compiler.push(
'{"op":"add","path":"/root","value":"main"}\n',
);
// Get the current spec at any time
const currentSpec = compiler.getSpec();
// Reset to start fresh
compiler.reset();
```
### StreamCompiler API
| Method | Returns | Description |
| ------------- | -------------- | --------------------------------------------------------------------------------- |
| `push(chunk)` | `Spec \| null` | Feed a text chunk. Returns updated spec if any patches applied, `null` otherwise. |
| `getSpec()` | `Spec \| null` | Current spec, or `null` if nothing received yet. |
| `reset()` | `void` | Clear the spec and internal line buffer. |
The compiler correctly handles text chunks that split across line boundaries. It buffers incomplete lines internally and only processes complete lines (delimited by `\n`).
## Example: Manual Fetch + ReadableStream
```tsx
"use client";
import { useState } from "react";
import { createStreamCompiler } from "@prototyperco/compose/core";
import { Renderer } from "@prototyperco/compose";
import { prototyperComponents } from "@prototyperco/compose/components";
import type { Spec } from "@prototyperco/compose/core";
export function ManualStreamExample() {
const [spec, setSpec] = useState(null);
async function generate() {
const compiler = createStreamCompiler();
const response = await fetch("/api/compose", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ prompt: "Build a pricing table" }),
});
const reader = response
.body!.pipeThrough(new TextDecoderStream())
.getReader();
while (true) {
const { done, value } = await reader.read();
if (done) break;
const updated = compiler.push(value);
if (updated) {
setSpec(updated);
}
}
// Flush any remaining buffered content
const final = compiler.push("\n");
if (final) setSpec(final);
}
return (
Generate
{spec && }
);
}
```
## Example: Server-Side API Route
Here is a minimal Next.js API route that streams JSONL from an AI model:
```ts
// app/api/compose/route.ts
import { NextRequest } from "next/server";
export async function POST(req: NextRequest) {
const { prompt } = await req.json();
// Call your AI model here. This example shows the response format.
const patches = [
{ op: "add", path: "/root", value: "card" },
{
op: "add",
path: "/elements/card",
value: { type: "Card", props: {}, children: ["heading", "text"] },
},
{
op: "add",
path: "/elements/heading",
value: { type: "Heading", props: { text: "Response", level: 2 } },
},
{
op: "add",
path: "/elements/text",
value: { type: "Text", props: { content: `You asked: ${prompt}` } },
},
];
const stream = new ReadableStream({
start(controller) {
const encoder = new TextEncoder();
for (const patch of patches) {
controller.enqueue(encoder.encode(JSON.stringify(patch) + "\n"));
}
controller.close();
},
});
return new Response(stream, {
headers: { "Content-Type": "application/x-ndjson" },
});
}
```
In a real integration, you would stream patches as your AI model generates them, writing each line to the response as it becomes available.
## Error Handling
The stream compiler is resilient to malformed input:
- Empty lines are silently skipped
- Lines that fail JSON parsing are silently skipped
- Lines with invalid patch operations (missing `op` or `path`) are silently skipped
- Valid patches are applied regardless of surrounding invalid lines
The `useUIStream` hook captures HTTP errors and stream errors in its `error` field. Abort errors (from calling `send()` while streaming) are ignored.
## Next Steps
- [Spec Format](/docs/compose/spec-format) — Understand what the patches are building
- [Actions](/docs/compose/actions) — Add interactivity to streamed UIs
- [Expressions](/docs/compose/expressions) — Dynamic data binding in props
# Type Safety
> TypeScript inference, type-safe spec building, and structured output schemas.
URL: https://prototyper-ui.com/docs/compose/type-safety
The catalog system is fully typed end-to-end. TypeScript infers component props and action params directly from your Zod definitions, and utility types let you extract those types for use elsewhere in your codebase.
## Type Inference Utilities
Four utility types are exported from `@prototyperco/compose/catalog` for extracting types from a compiled catalog:
```ts
import type {
InferComponentProps,
InferActionParams,
InferCatalogComponents,
InferCatalogActions,
} from "@prototyperco/compose/catalog";
```
### InferComponentProps
Extract the inferred props type for a specific component by name. The result is the `z.infer` of that component's Zod props schema.
```ts
type ButtonProps = InferComponentProps;
// → { label: string; variant?: "default" | "destructive" | "outline" | "ghost" }
type InputProps = InferComponentProps;
// → { label?: string; placeholder?: string; type?: "text" | "email" | "password" }
```
### InferActionParams
Extract the inferred params type for a specific action by name:
```ts
type SubmitParams = InferActionParams;
// → { formId: string; validate?: boolean }
```
### InferCatalogComponents / InferCatalogActions
Extract the full component or action maps from a catalog. Useful for building generic utilities that operate over all catalog entries:
```ts
type Components = InferCatalogComponents;
// → { Button: ComponentDefinition<...>; Card: ComponentDefinition<...>; ... }
type ComponentNames = keyof InferCatalogComponents;
// → "Button" | "Card" | "Input" | ...
type Actions = InferCatalogActions;
// → { submitForm: ActionDefinition<...>; ... }
```
## Type-Safe Spec Builder
`createSpecBuilder()` returns a builder object that constrains element types to components registered in your catalog. If you pass an invalid component name, TypeScript reports an error at compile time.
```ts
import { createSpecBuilder } from "@prototyperco/compose/catalog";
const builder = createSpecBuilder(catalog);
```
### builder.element()
Create a named `UIElement` entry. The `type` parameter is constrained to component names from the catalog:
```ts
const btn = builder.element("btn", "Button", { label: "Click me" });
// ✓ "Button" is a valid component name
// @ts-expect-error — "Buttton" is not in the catalog
const bad = builder.element("x", "Buttton", {});
```
The fourth argument accepts optional `children`, `visible`, and `on` bindings:
```ts
const card = builder.element(
"card",
"Card",
{ title: "Welcome" },
{
children: ["btn", "input"],
visible: { $state: "/showCard" },
on: {
press: { action: "submitForm", params: { formId: "main" } },
},
},
);
```
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `key` | `string` | - | Unique element key within the spec |
| `type` | `keyof Components` | - | Component type (must exist in the catalog) |
| `props` | `Record` | - | Component props |
### builder.spec()
Assemble a complete `Spec` from named elements:
```ts
const spec = builder.spec(
"card",
{
card: card.element,
btn: btn.element,
},
{ showCard: true }, // optional initial state
);
```
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `root` | `string` | - | Key of the root element |
| `elements` | `Record` | - | Flat map of element keys to UIElements |
| `state` | `Record` | - | Optional initial state model |
## Structured Output Schemas
`catalog.jsonSchema()` returns the full spec as a JSON Schema object, ready for use with any AI provider's structured output feature. `catalog.zodSchema()` returns the underlying Zod schema.
### OpenAI Structured Outputs
```ts
const jsonSchema = catalog.jsonSchema();
const response = await openai.chat.completions.create({
model: "gpt-4o",
messages: [
{ role: "system", content: systemPrompt },
{ role: "user", content: "Build a login form" },
],
response_format: {
type: "json_schema",
json_schema: { name: "ui_spec", schema: jsonSchema },
},
});
```
### Anthropic Tool Use
```ts
const jsonSchema = catalog.jsonSchema();
const response = await anthropic.messages.create({
model: "claude-sonnet-4-20250514",
system: systemPrompt,
messages: [{ role: "user", content: "Build a login form" }],
tools: [
{
name: "generate_ui",
description: "Generate a UI spec matching the component catalog",
input_schema: jsonSchema,
},
],
});
```
### Per-Component Schemas
`catalog.componentSchemas()` returns individual JSON Schemas for each component and action, useful for documentation or granular validation:
```ts
const schemas = catalog.componentSchemas();
// {
// "$schema": "http://json-schema.org/draft-07/schema#",
// "components": {
// "Button": { "type": "object", "properties": { ... } },
// "Card": { ... },
// },
// "actions": {
// "submitForm": { ... },
// },
// }
```
## Runtime Validation
After receiving AI output, validate it against the catalog before rendering:
```ts
const result = catalog.validate(aiOutput)
if (result.valid) {
// result.data contains the parsed Spec
return
} else {
console.error("Invalid spec:", result.issues)
}
```
The `validate()` method performs two-pass validation:
1. **Structural validation** — checks the spec shape, root reference, and component types via the Zod schema
2. **Per-element prop validation** — checks each element's props against its component's Zod schema (expression objects are stripped before validation since they resolve at runtime)
The result includes a `valid` boolean, the parsed `data` (when valid), and an `issues` array with detailed error information:
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `valid` | `boolean` | - | Whether the spec passed structural validation |
| `data` | `Spec \| undefined` | - | Parsed spec (present when valid is true) |
| `issues` | `SpecIssue[]` | - | Array of validation issues with code, message, severity, and optional elementKey |
### Auto-Fixing Invalid Specs
When validation fails, `autoFixSpec()` can attempt to repair common issues in the spec:
```ts
import { autoFixSpec } from "@prototyperco/compose"
const result = catalog.validate(aiOutput)
if (!result.valid) {
const { spec: fixed, fixes } = autoFixSpec(aiOutput)
console.log("Applied fixes:", fixes)
return
}
```
## Expression Schemas
For advanced use cases, the `dynamicOf()` helper wraps any Zod schema to also accept dynamic expression objects. This is used internally by the catalog to validate props that can be either literal values or runtime expressions.
```ts
import { dynamicOf, expressionSchema } from "@prototyperco/compose/catalog";
import { z } from "zod";
// A prop that accepts a literal string OR any expression
const dynamicString = dynamicOf(z.string());
dynamicString.parse("hello"); // literal string
dynamicString.parse({ $state: "/user/name" }); // state reference
dynamicString.parse({ $template: "Hi ${/name}" }); // template expression
dynamicString.parse({ $cond: { $state: "/x" }, $then: "a", $else: "b" }); // conditional
```
The `expressionSchema` itself is a union of all expression types:
| Expression | Schema | Description |
| ------------------------- | --------------------------- | --------------------------------- |
| `{ $state: string }` | `stateExpressionSchema` | Read a value from the state model |
| `{ $item: string }` | `itemExpressionSchema` | Read from the current repeat item |
| `{ $index: true }` | `indexExpressionSchema` | Current repeat index |
| `{ $bindState: string }` | `bindStateExpressionSchema` | Two-way state binding |
| `{ $bindItem: string }` | `bindItemExpressionSchema` | Two-way item binding |
| `{ $template: string }` | `templateExpressionSchema` | String interpolation |
| `{ $computed: string }` | `computedExpressionSchema` | Computed value with optional args |
| `{ $cond, $then, $else }` | `condExpressionSchema` | Conditional value |
All expression schemas are exported individually from `@prototyperco/compose/catalog` for use in custom validation logic.
### Using dynamicOf in Component Definitions
When defining component props that should accept dynamic values, wrap the base schema with `dynamicOf()`:
```ts
import { z } from "zod";
import { defineComponent } from "@prototyperco/compose/catalog";
import { dynamicOf } from "@prototyperco/compose/catalog";
export default defineComponent({
description: "A text display component",
props: z.object({
text: dynamicOf(z.string()), // accepts "hello" or { $state: "/msg" }
visible: dynamicOf(z.boolean()).optional(),
}),
});
```
# Validation
> Built-in field validators and custom validation rules
URL: https://prototyper-ui.com/docs/compose/validation
Compose includes a declarative validation system with 14 built-in validators, cross-field validation, and conditional rules. Validators run client-side and integrate with the `useFieldValidation()` hook for real-time feedback.
## Validation Structure
Each field's validation is configured with a `ValidationConfig` object containing an array of checks and a timing strategy.
```json
{
"validation": {
"checks": [
{ "validator": "required", "message": "Email is required" },
{ "validator": "email", "message": "Enter a valid email address" }
],
"validateOn": "change"
}
}
```
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `checks` | `ValidationCheck[]` | - | Array of validation checks to run |
| `validateOn` | `"change" \| "blur" \| "submit"` | `"change"` | When to run validation. Defaults to "change" |
### ValidationCheck
Each check references a named validator (built-in or custom), a user-facing error message, optional arguments, and an optional `enabled` condition.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `validator` | `string` | - | Name of the validator function (built-in or custom) |
| `message` | `string` | - | Error message displayed on failure |
| `args` | `Record` | - | Arguments passed to the validator function |
| `enabled` | `VisibilityCondition` | - | Condition that must be true for this check to run |
## Built-in Validators
All built-in validators skip validation when the value is empty (null, undefined, empty string, or empty array), except `required` and `requiredIf`. This means optional fields only validate when the user has entered something.
### required
Value must be non-null, non-undefined, non-empty string, and non-empty array.
```json
{ "validator": "required", "message": "This field is required" }
```
### email
Basic email format check (`user@domain.tld`).
```json
{ "validator": "email", "message": "Enter a valid email address" }
```
### minLength
String or array length must be at least `args.min`.
```json
{
"validator": "minLength",
"message": "Must be at least 8 characters",
"args": { "min": 8 }
}
```
### maxLength
String or array length must be at most `args.max`.
```json
{
"validator": "maxLength",
"message": "Cannot exceed 100 characters",
"args": { "max": 100 }
}
```
### pattern
Value must match the regex provided in `args.pattern`.
```json
{
"validator": "pattern",
"message": "Only letters and numbers allowed",
"args": { "pattern": "^[a-zA-Z0-9]+$" }
}
```
### min
Numeric value must be greater than or equal to `args.min`.
```json
{ "validator": "min", "message": "Must be at least 18", "args": { "min": 18 } }
```
### max
Numeric value must be less than or equal to `args.max`.
```json
{ "validator": "max", "message": "Must be 100 or less", "args": { "max": 100 } }
```
### numeric
Value must be a valid number (number type, or a string parseable as a number).
```json
{ "validator": "numeric", "message": "Must be a number" }
```
### url
Value must match the URL pattern (`protocol://host`).
```json
{ "validator": "url", "message": "Enter a valid URL" }
```
### matches
Value must equal the value of another field at `args.path`. Useful for confirm-password fields.
```json
{
"validator": "matches",
"message": "Passwords do not match",
"args": { "path": "/password" }
}
```
### equalTo
Value must strictly equal `args.value`.
```json
{
"validator": "equalTo",
"message": "Must agree to terms",
"args": { "value": true }
}
```
### lessThan
Numeric value must be less than the value at `args.path`. Useful for range validation (e.g., start date before end date).
```json
{
"validator": "lessThan",
"message": "Start must be less than end",
"args": { "path": "/endValue" }
}
```
### greaterThan
Numeric value must be greater than the value at `args.path`.
```json
{
"validator": "greaterThan",
"message": "Must be greater than minimum",
"args": { "path": "/minValue" }
}
```
### requiredIf
Value is required only when the field at `args.path` is truthy. When the condition field is falsy, this check always passes.
```json
{
"validator": "requiredIf",
"message": "Address is required for shipping",
"args": { "path": "/needsShipping" }
}
```
## Validation Timing
The `validateOn` property controls when validation runs:
| Value | Behavior |
| ---------- | -------------------------------------------------------------------------- |
| `"change"` | Validates on every state change after the field is first touched (default) |
| `"blur"` | Validates when the field loses focus (via the `touch()` callback) |
| `"submit"` | Validates only when `validate()` is called manually |
## Conditional Validation
Use the `enabled` field on a check to make it conditional. It accepts any `VisibilityCondition` value:
```json
{
"checks": [
{
"validator": "required",
"message": "Company name is required for business accounts",
"enabled": { "$state": "/accountType", "eq": "business" }
}
]
}
```
When `enabled` evaluates to false, the check is skipped entirely (always passes). When omitted, the check always runs.
## useFieldValidation() Hook
In React, the `useFieldValidation()` hook connects validation to a bound state path.
```tsx
import { useFieldValidation } from "@prototyperco/compose";
function EmailField() {
const { errors, validate, touch, clear } = useFieldValidation("/form/email", {
checks: [
{ validator: "required", message: "Email is required" },
{ validator: "email", message: "Enter a valid email" },
],
validateOn: "change",
});
return (
touch()} />
{errors.map((err) => (
{err}
))}
);
}
```
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `errors` | `string[]` | - | Current validation error messages |
| `validate` | `() => void` | - | Manually run all validation checks |
| `touch` | `() => void` | - | Mark the field as touched. Triggers blur validation if configured |
| `clear` | `() => void` | - | Clear all errors and reset touched state |
The hook automatically re-validates on state changes when the field has been touched (for `"change"` mode). For `"blur"` mode, it validates when `touch()` is called. For `"submit"` mode, call `validate()` explicitly when the form is submitted.
## Complete Form Example
A registration form spec with multiple validators, cross-field matching, and conditional required fields:
```json
{
"root": "form",
"elements": {
"form": {
"type": "Card",
"props": {},
"children": [
"emailField",
"passwordField",
"confirmField",
"typeField",
"companyField",
"submitBtn"
]
},
"emailField": {
"type": "Input",
"props": {
"label": "Email",
"value": { "$bindState": "/form/email" },
"validation": {
"checks": [
{ "validator": "required", "message": "Email is required" },
{ "validator": "email", "message": "Enter a valid email" }
],
"validateOn": "blur"
}
}
},
"passwordField": {
"type": "Input",
"props": {
"label": "Password",
"type": "password",
"value": { "$bindState": "/form/password" },
"validation": {
"checks": [
{ "validator": "required", "message": "Password is required" },
{
"validator": "minLength",
"message": "At least 8 characters",
"args": { "min": 8 }
},
{
"validator": "pattern",
"message": "Must contain a number",
"args": { "pattern": "\\d" }
}
]
}
}
},
"confirmField": {
"type": "Input",
"props": {
"label": "Confirm Password",
"type": "password",
"value": { "$bindState": "/form/confirmPassword" },
"validation": {
"checks": [
{
"validator": "required",
"message": "Please confirm your password"
},
{
"validator": "matches",
"message": "Passwords do not match",
"args": { "path": "/form/password" }
}
]
}
}
},
"typeField": {
"type": "Select",
"props": {
"label": "Account Type",
"value": { "$bindState": "/form/accountType" },
"options": ["personal", "business"]
}
},
"companyField": {
"type": "Input",
"props": {
"label": "Company Name",
"value": { "$bindState": "/form/company" },
"validation": {
"checks": [
{
"validator": "requiredIf",
"message": "Required for business accounts",
"args": { "path": "/form/accountType" },
"enabled": { "$state": "/form/accountType", "eq": "business" }
}
]
}
}
},
"submitBtn": {
"type": "Button",
"props": { "label": "Register" },
"on": { "press": { "action": "submitForm" } }
}
},
"state": {
"form": {
"email": "",
"password": "",
"confirmPassword": "",
"accountType": "personal",
"company": ""
}
}
}
```
## Dynamic Validation Args
Validation arguments can reference other state values using `{ $state: "/path" }` instead of literal values. This enables cross-field validation where the threshold itself is dynamic.
For example, a `min` validator whose minimum comes from another field:
```json
{
"validator": "min",
"message": "Must be at least the minimum",
"args": { "min": { "$state": "/settings/minValue" } }
}
```
At validation time, `{ "$state": "/settings/minValue" }` resolves to whatever number is currently stored at that state path. If `/settings/minValue` is `10`, the check behaves exactly like `"args": { "min": 10 }`.
Any argument value in `args` can be a `DynamicValue` — either a literal or a `{ $state }` reference. This works with all validators that accept arguments (`min`, `max`, `minLength`, `maxLength`, `pattern`, `matches`, `equalTo`, `lessThan`, `greaterThan`, `requiredIf`).
### Example: Cross-Field Password Matching with Dynamic Args
A registration form where the "confirm password" field dynamically references the password field:
```json
{
"confirmField": {
"type": "Input",
"props": {
"label": "Confirm Password",
"type": "password",
"value": { "$bindState": "/form/confirmPassword" },
"validation": {
"checks": [
{
"validator": "required",
"message": "Please confirm your password"
},
{
"validator": "matches",
"message": "Passwords do not match",
"args": { "path": { "$state": "/form/password" } }
}
]
}
}
}
}
```
The `path` argument resolves dynamically, so the validator always compares against the current password value.
## Validation Helpers
When building specs in TypeScript, the `check` helper provides a cleaner API with sensible default messages. Each method returns a properly shaped `ValidationCheck` object:
```ts
import { check } from "@prototyperco/compose/core";
const emailChecks = [
check.required(), // "This field is required"
check.email(), // "Invalid email address"
];
const passwordChecks = [
check.required(),
check.minLength(8), // "Must be at least 8 characters"
check.pattern("\\d", "Must contain a number"),
];
const confirmChecks = [
check.required(),
check.matches("/form/password", "Passwords do not match"),
];
// Dynamic args are supported too:
const rangeChecks = [
check.min({ $state: "/settings/minValue" }, "Below minimum"),
check.max({ $state: "/settings/maxValue" }, "Above maximum"),
];
```
Available helpers: `check.required()`, `check.email()`, `check.minLength(n)`, `check.maxLength(n)`, `check.pattern(regex)`, `check.min(n)`, `check.max(n)`, `check.numeric()`, `check.url()`, `check.matches(path)`, `check.equalTo(value)`, `check.lessThan(path)`, `check.greaterThan(path)`, `check.requiredIf(path)`.
See the [API Reference](/docs/compose/api-reference) for complete signatures.
## Programmatic Validation
For custom validation logic outside of the hook, use `runValidation()` directly:
```ts
import { runValidation, builtInValidators } from "@prototyperco/compose/core";
const checks = [
{ validator: "required", message: "Required" },
{ validator: "email", message: "Invalid email" },
];
const result = runValidation(checks, "test@example.com");
// { valid: true, errors: [] }
const result2 = runValidation(checks, "not-an-email");
// { valid: false, errors: ["Invalid email"] }
```
For cross-field validation, pass a `ValidationContext`:
```ts
const ctx = {
stateModel: { password: "secret123", confirmPassword: "secret456" },
getFieldValue: (path: string) => getByPath(ctx.stateModel, path),
};
const checks = [
{
validator: "matches",
message: "Passwords must match",
args: { path: "/password" },
},
];
const result = runValidation(checks, "secret456", undefined, ctx);
// { valid: false, errors: ["Passwords must match"] }
```
# Conditional Rendering
> Show and hide elements based on state, repeat context, and compound logic
URL: https://prototyper-ui.com/docs/compose/visibility
The `visible` field on any element controls whether it renders. When `visible` evaluates to `false`, the element and all its children are removed from the tree entirely (not just hidden with CSS).
## Basic Usage
The simplest condition checks if a state value is truthy:
```json
{
"type": "Alert",
"props": { "title": "Welcome back!" },
"visible": { "$state": "/user/isLoggedIn" }
}
```
This element renders only when `/user/isLoggedIn` is truthy (not `null`, `undefined`, `false`, `0`, or `""`).
## State Conditions
A state condition reads a value from the global state model:
```ts
type StateCondition = {
$state: string; // JSON Pointer path
eq?: unknown; // Strict equality
neq?: unknown; // Strict inequality
gt?: number; // Greater than
gte?: number; // Greater than or equal
lt?: number; // Less than
lte?: number; // Less than or equal
not?: boolean; // Invert the result
};
```
### Truthy Check (Default)
When no comparison operator is specified, the condition checks truthiness:
```json
{ "$state": "/user/name" }
```
Renders when `/user/name` is a non-empty value.
### Equality
```json
{ "$state": "/currentTab", "eq": "settings" }
```
Renders only when `/currentTab` is exactly `"settings"`.
### Inequality
```json
{ "$state": "/status", "neq": "loading" }
```
Renders when `/status` is anything other than `"loading"`.
### Numeric Comparisons
```json
{ "$state": "/cart/itemCount", "gt": 0 }
{ "$state": "/user/age", "gte": 18 }
{ "$state": "/inventory/stock", "lt": 10 }
{ "$state": "/form/progress", "lte": 100 }
```
### Negation with `not`
The `not` flag inverts the final result of any condition:
```json
{ "$state": "/user/isLoggedIn", "not": true }
```
This renders when the user is NOT logged in. `not` is applied after the comparison operator, so you can combine them:
```json
{ "$state": "/status", "eq": "error", "not": true }
```
Renders when `/status` is anything other than `"error"` (equivalent to `neq`).
## Item Conditions (Repeat Context)
Inside a [repeat](/docs/compose/spec-format#repeat-binding) scope, conditions can reference the current item:
```ts
type ItemCondition = {
$item: string; // Field name on the repeat item
eq?: unknown;
neq?: unknown;
gt?: number;
gte?: number;
lt?: number;
lte?: number;
not?: boolean;
};
```
```json
{
"type": "Badge",
"props": { "variant": "default", "label": "Complete" },
"visible": { "$item": "done", "eq": true }
}
```
This badge renders only for items where `done` is `true`.
## Index Conditions (Repeat Context)
Conditions on the current repeat index:
```json
{
"type": "Separator",
"props": {},
"visible": { "$index": true, "gt": 0 }
}
```
This separator renders for every item except the first (index > 0), creating dividers between items.
## Boolean Literals
For explicit always-on or always-off visibility:
```json
{ "visible": true }
{ "visible": false }
```
`true` is the default when `visible` is omitted. Setting `visible: false` permanently hides an element.
## Logical Operators
### Implicit AND (Array)
Pass an array of conditions. All must be true:
```json
{
"visible": [
{ "$state": "/user/isLoggedIn" },
{ "$state": "/user/role", "eq": "admin" }
]
}
```
Renders only when the user is logged in AND is an admin.
### Explicit AND
The `$and` operator is equivalent to an array but more explicit:
```json
{
"visible": {
"$and": [
{ "$state": "/user/isLoggedIn" },
{ "$state": "/user/role", "eq": "admin" },
{ "$state": "/feature/adminPanel" }
]
}
}
```
### Explicit OR
The `$or` operator requires at least one condition to be true:
```json
{
"visible": {
"$or": [
{ "$state": "/user/role", "eq": "admin" },
{ "$state": "/user/role", "eq": "moderator" }
]
}
}
```
Renders for admins OR moderators.
### Combining AND and OR
Logical operators can be nested for complex conditions:
```json
{
"visible": {
"$and": [
{ "$state": "/user/isLoggedIn" },
{
"$or": [
{ "$state": "/user/role", "eq": "admin" },
{ "$state": "/user/permissions/canEdit" }
]
}
]
}
}
```
Renders when the user is logged in AND (is an admin OR has edit permissions).
## Examples
### Tab Panel Switching
Show content based on the active tab:
```json
{
"root": "container",
"elements": {
"container": {
"type": "Card",
"props": {},
"children": ["tabs", "profile-panel", "settings-panel", "billing-panel"]
},
"tabs": {
"type": "Tabs",
"props": {
"value": { "$bindState": "/activeTab" },
"items": [
{ "value": "profile", "label": "Profile" },
{ "value": "settings", "label": "Settings" },
{ "value": "billing", "label": "Billing" }
]
}
},
"profile-panel": {
"type": "Text",
"props": { "content": "Profile content here" },
"visible": { "$state": "/activeTab", "eq": "profile" }
},
"settings-panel": {
"type": "Text",
"props": { "content": "Settings content here" },
"visible": { "$state": "/activeTab", "eq": "settings" }
},
"billing-panel": {
"type": "Text",
"props": { "content": "Billing content here" },
"visible": { "$state": "/activeTab", "eq": "billing" }
}
},
"state": { "activeTab": "profile" }
}
```
### Form State Toggle
Show a success message after submission, hide the form:
```json
{
"type": "Card",
"props": {},
"children": ["email-input", "submit-btn"],
"visible": { "$state": "/submitted", "not": true }
}
```
```json
{
"type": "Alert",
"props": { "title": "Thanks for subscribing!" },
"visible": { "$state": "/submitted" }
}
```
### Authentication Gate
Show different content for logged-in vs. anonymous users:
```json
{
"type": "Button",
"props": { "label": "Sign In" },
"visible": { "$state": "/user/isLoggedIn", "not": true }
}
```
```json
{
"type": "Text",
"props": { "content": { "$template": "Welcome, ${/user/name}!" } },
"visible": { "$state": "/user/isLoggedIn" }
}
```
### Repeat Context Filtering
Show a "completed" badge only for done items, and a delete button only for items the user owns:
```json
{
"type": "Badge",
"props": { "label": "Done", "variant": "default" },
"visible": { "$item": "done" }
}
```
```json
{
"type": "Button",
"props": { "label": "Delete", "variant": "destructive", "size": "sm" },
"visible": {
"$and": [
{ "$item": "ownerId", "eq": { "$state": "/currentUserId" } },
{ "$item": "done", "not": true }
]
}
}
```
## Dynamic State Comparisons
Comparison operators (`gt`, `gte`, `lt`, `lte`) accept a `ComparisonValue`, which can be either a literal number or a `{ $state: "/path" }` reference. This enables comparisons between two state values at runtime.
```ts
type ComparisonValue = number | { $state: string };
```
For example, show a product only when its price is within the user's budget:
```json
{
"type": "Card",
"props": { "title": "Premium Plan" },
"visible": { "$state": "/price", "lte": { "$state": "/budget" } }
}
```
At evaluation time, both sides resolve to numbers from state. If `/price` is `49` and `/budget` is `100`, the condition evaluates as `49 <= 100` (true).
Dynamic comparisons work with all numeric operators:
```json
{ "$state": "/score", "gt": { "$state": "/threshold" } }
{ "$state": "/quantity", "gte": { "$state": "/minOrder" } }
{ "$state": "/temperature", "lt": { "$state": "/maxTemp" } }
{ "$state": "/balance", "lte": { "$state": "/creditLimit" } }
```
### Example: Product Filtering by Budget
A product list where items are visible only when their price falls within the user's budget:
```json
{
"root": "container",
"elements": {
"container": {
"type": "Card",
"props": {},
"children": ["budgetSlider", "productList"]
},
"budgetSlider": {
"type": "Slider",
"props": {
"label": "Max Budget",
"min": 0,
"max": 500,
"value": { "$bindState": "/budget" }
}
},
"productList": {
"type": "Card",
"props": {},
"children": ["product-a", "product-b", "product-c"]
},
"product-a": {
"type": "Text",
"props": { "content": "Basic Plan — $29" },
"visible": { "$state": "/prices/basic", "lte": { "$state": "/budget" } }
},
"product-b": {
"type": "Text",
"props": { "content": "Pro Plan — $99" },
"visible": { "$state": "/prices/pro", "lte": { "$state": "/budget" } }
},
"product-c": {
"type": "Text",
"props": { "content": "Enterprise Plan — $299" },
"visible": {
"$state": "/prices/enterprise",
"lte": { "$state": "/budget" }
}
}
},
"state": {
"budget": 200,
"prices": { "basic": 29, "pro": 99, "enterprise": 299 }
}
}
```
As the user moves the budget slider, products dynamically show or hide based on whether their price is within budget.
## Item & Index Helpers
When building specs in TypeScript, the `visibility` helper includes `item` and `index` sub-objects for constructing repeat-context conditions:
```ts
import { visibility } from "@prototyperco/compose/core";
// Item conditions — reference a field on the current repeat item
visibility.item.when("isActive"); // truthy check
visibility.item.unless("isDeleted"); // falsy check (not: true)
visibility.item.eq("status", "active"); // equality
visibility.item.neq("status", "draft"); // inequality
visibility.item.gt("priority", 5); // greater than
visibility.item.gte("score", 80); // greater than or equal
visibility.item.lt("price", { $state: "/maxPrice" }); // less than (dynamic)
visibility.item.lte("quantity", 0); // less than or equal
// Index conditions — reference the current repeat index
visibility.index.eq(0); // first item only
visibility.index.neq(0); // skip first item
visibility.index.gt(0); // all except first
visibility.index.lt(5); // first 5 items
visibility.index.gte(2); // from third item onward
visibility.index.lte(9); // first 10 items
```
These helpers return properly typed `ItemCondition` and `IndexCondition` objects. Combine them with `visibility.and()` and `visibility.or()` for compound conditions:
```ts
// Show a separator between items (not before the first)
const separatorVisible = visibility.index.gt(0);
// Show delete button only for owned, incomplete items
const deleteVisible = visibility.and(
visibility.item.eq("ownerId", currentUserId),
visibility.item.unless("done"),
);
```
## Visibility vs. `$cond`
Both `visible` and `$cond` use the same condition system, but they serve different purposes:
| Feature | `visible` | `$cond` |
| ------------ | ----------------------------------- | ------------------------------------ |
| **Scope** | Controls whether an element renders | Controls the value of a single prop |
| **Result** | Element in tree or not | `$then` value or `$else` value |
| **Use when** | Showing/hiding entire sections | Switching a variant, label, or style |
Use `visible` to remove elements from the tree. Use `$cond` in props to change appearance without removing elements:
```json
{
"type": "Badge",
"props": {
"variant": {
"$cond": { "$state": "/status", "eq": "active" },
"$then": "default",
"$else": "secondary"
},
"label": { "$state": "/status" }
}
}
```
## Programmatic Visibility Helpers
When building specs in TypeScript, use the `visibility` helper for a cleaner API:
```tsx
import { visibility } from "@prototyperco/compose/core"
const element = {
type: "Alert",
props: { title: "Admin Panel" },
visible: visibility.and(
visibility.when("/user/isLoggedIn"),
visibility.eq("/user/role", "admin"),
),
}
// Other helpers:
visibility.always // true
visibility.never // false
visibility.when("/path") // truthy check
visibility.unless("/path") // falsy check (not: true)
visibility.eq("/path", v) // equality
visibility.neq("/path", v) // inequality
visibility.gt("/path", n) // greater than
visibility.gte("/path", n) // greater than or equal
visibility.lt("/path", n) // less than
visibility.lte("/path", n) // less than or equal
visibility.and(...) // all must be true
visibility.or(...) // at least one must be true
```
## Next Steps
- [Expressions](/docs/compose/expressions) — `$cond` for conditional prop values
- [Actions](/docs/compose/actions) — Trigger state changes that affect visibility
- [Spec Format](/docs/compose/spec-format) — Full spec structure reference
# Live Canvas
> Real-time AI-to-browser design system. Watch AI agents build interfaces live in your browser with collaborative CRDT sync.
URL: https://prototyper-ui.com/docs/design-bridge
The Live Canvas connects AI agents to a shared browser preview. When an agent creates or modifies a design through MCP tools, the changes appear instantly in your browser — no page reload, no polling.
## Key Features
- **Real-time preview** — See every change as the AI makes it, streamed to the browser via Yjs CRDT sync
- **Operation-based updates** — Granular spec operations (add element, set prop, reorder children) instead of full replacements
- **Collaborative** — Multiple browser tabs and AI agents can view and edit the same session simultaneously
- **Conflict-free** — Yjs CRDT merges concurrent edits automatically, even across agents
- **Agent presence** — See which AI agent is editing what, with live status indicators
- **Theme-aware** — Full OKLCH theme system with natural language descriptions and presets
- **Export to code** — When the design is ready, export as a React component with theme CSS
## Quick Start
**1. Configure the MCP server**
```bash
claude mcp add prototyper-ui -- npx -y @prototyperco/mcp@latest
```
The MCP server uses the hosted bridge by default. For offline or low-latency local work, run a local bridge and set `PROTOTYPER_BRIDGE_URL`.
**2. Create a session from your AI assistant**
Use the `design_create` MCP tool in Claude Code, Cursor, or any MCP-compatible client:
```
Create a login page with email and password fields
```
The AI calls `design_create`, which returns a `previewUrl`. Open it in your browser.
**3. Watch it build**
As the AI calls `design_update` to add components, change props, and adjust the theme, you see every change live in the browser.
## Architecture
```
AI Agent (MCP) Bridge Server Browser
┌──────────────┐ ┌────────────────────┐ ┌──────────────────┐
│ │ │ │ │ │
│ design_create├──HTTP──> │ REST API │ │ /design?session │
│ design_update├──HTTP──> │ POST /api/sessions │ │ │
│ design_theme ├──HTTP──> │ POST /sessions/ │ │ ┌──────────────┐ │
│ design_get ├──HTTP──> │ :id/ops │ │ │ Y.Doc (CRDT) │ │
│ design_export│<──HTTP── │ POST /sessions/ │ │ │ ├─ spec │ │
│ │ │ :id/presence │ │ │ ├─ theme │ │
│ │ │ │ │ │ └─ meta │ │
│ │ │ Yjs / Hocuspocus │ │ └──────┬───────┘ │
│ │ │ (WebSocket CRDT) │<─WSS─>│ HocuspocusProvider│
│ │ │ │ │ │
│ │ │ Hosted persistence │ │ Live Renderer │
│ │ │ or local bridge │ │ Agent Presence UI│
└──────────────┘ └────────────────────┘ └──────────────────┘
```
The bridge server holds a Yjs `Y.Doc` per session. AI agents write via the REST API, which mutates the Y.Doc. Browsers connect over WebSocket and receive CRDT updates in real time. Because both sides operate on the same CRDT document, there is no state drift.
## Data Model
Each session stores its state in a Yjs document:
| Yjs Map | Contents |
| ---------- | --------------------------------------------------- |
| `spec` | `root` (string) + `elements` (nested Y.Maps) |
| `theme` | `hue`, `chroma`, `grayChroma`, `radius`, `font` |
| `themeCSS` | Generated OKLCH token CSS |
| `meta` | `id`, `name`, `description`, `revision`, timestamps |
Elements inside `spec.elements` are themselves Y.Maps with `type`, `props` (Y.Map), and `children` (Y.Array), so concurrent edits to different props on the same element merge automatically.
## Next Steps
- [Getting Started](/docs/design-bridge/getting-started) — Set up the canvas and create your first session
- [MCP Tools Reference](/docs/design-bridge/mcp-tools) — Full reference for all 7 design tools
- [API Reference](/docs/design-bridge/api-reference) — REST endpoints, WebSocket protocol, and curl examples
# Live Canvas API Reference
> REST API and WebSocket protocol reference for the Live Canvas server.
URL: https://prototyper-ui.com/docs/design-bridge/api-reference
The bridge server exposes a REST API for session management and a WebSocket endpoint for real-time Yjs CRDT sync. The MCP server uses the hosted bridge by default: `https://aa-prototyper-bridge.fly.dev`. For local bridge development, use `http://localhost:4321`; the examples below use the local URL.
## Endpoints Overview
| Method | Path | Description |
| -------- | ---------------------------- | ---------------------- |
| `GET` | `/health` | Health check |
| `POST` | `/api/sessions` | Create a session |
| `GET` | `/api/sessions` | List all sessions |
| `GET` | `/api/sessions/:id` | Get session metadata |
| `GET` | `/api/sessions/:id/state` | Get full session state |
| `POST` | `/api/sessions/:id/ops` | Apply spec operations |
| `POST` | `/api/sessions/:id/presence` | Update agent presence |
| `DELETE` | `/api/sessions/:id` | Delete a session |
| `WS` | `/ws` | Yjs WebSocket sync |
All JSON endpoints return `Content-Type: application/json` and include CORS headers (`Access-Control-Allow-Origin: *`).
---
## GET /health
Health check to verify the server is running.
**Response:**
```json
{ "ok": true }
```
**curl:**
```bash
curl http://localhost:4321/health
```
---
## POST /api/sessions
Create a new design session. Initializes a Yjs document with the provided spec and theme.
**Request body:**
| Field | Type | Required | Description |
| ------------- | ------------- | -------- | ------------------------------------ |
| `name` | `string` | No | Session name (default: `"Untitled"`) |
| `description` | `string` | No | Human-readable description |
| `spec` | `Spec` | No | Initial Compose spec |
| `theme` | `ThemeParams` | No | Initial theme parameters |
**Response (201):**
```json
{
"ok": true,
"session": {
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"name": "Login Page",
"description": "A login form",
"revision": 0,
"createdAt": "2026-03-25T10:00:00.000Z",
"updatedAt": "2026-03-25T10:00:00.000Z"
}
}
```
**curl:**
```bash
curl -X POST http://localhost:4321/api/sessions \
-H "Content-Type: application/json" \
-d '{
"name": "Login Page",
"description": "A login form with email and password",
"spec": {
"root": "container",
"elements": {
"container": {
"type": "Card",
"props": { "className": "p-6" },
"children": ["heading"]
},
"heading": {
"type": "Heading",
"props": { "level": 2, "children": "Login" }
}
}
}
}'
```
---
## GET /api/sessions
List all active sessions.
**Response (200):**
```json
{
"ok": true,
"sessions": [
{
"id": "a1b2c3d4-...",
"name": "Login Page",
"description": "A login form",
"revision": 5,
"createdAt": "2026-03-25T10:00:00.000Z",
"updatedAt": "2026-03-25T10:05:00.000Z"
}
]
}
```
**curl:**
```bash
curl http://localhost:4321/api/sessions
```
---
## GET /api/sessions/:id
Get metadata for a single session.
**Response (200):**
```json
{
"ok": true,
"session": {
"id": "a1b2c3d4-...",
"name": "Login Page",
"description": "A login form",
"revision": 5,
"createdAt": "2026-03-25T10:00:00.000Z",
"updatedAt": "2026-03-25T10:05:00.000Z"
}
}
```
**Response (404):**
```json
{ "error": "Session not found" }
```
**curl:**
```bash
curl http://localhost:4321/api/sessions/a1b2c3d4-e5f6-7890-abcd-ef1234567890
```
---
## GET /api/sessions/:id/state
Get the full session state including spec, theme, theme CSS, and metadata.
**Response (200):**
```json
{
"ok": true,
"spec": {
"root": "container",
"elements": {
"container": {
"type": "Card",
"props": { "className": "p-6" },
"children": ["heading", "form"]
}
}
},
"theme": {
"hue": 264,
"chroma": 0.24,
"grayChroma": 0.01,
"radius": 0.625
},
"themeCSS": ":root { --primary: 39.11% 0.084 264; ... }",
"meta": {
"id": "a1b2c3d4-...",
"name": "Login Page",
"description": "A login form",
"revision": 5,
"createdAt": "2026-03-25T10:00:00.000Z",
"updatedAt": "2026-03-25T10:05:00.000Z"
}
}
```
**curl:**
```bash
curl http://localhost:4321/api/sessions/a1b2c3d4-e5f6-7890-abcd-ef1234567890/state
```
---
## POST /api/sessions/:id/ops
Apply spec operations to a session. Operations are applied to the Yjs document and synced to all connected browsers immediately.
**Request body:**
| Field | Type | Required | Description |
| --------- | ---------- | -------- | -------------------------- |
| `ops` | `SpecOp[]` | Yes | Array of spec operations |
| `agentId` | `string` | No | Agent identifier for audit |
### SpecOp Types
```typescript
type SpecOp =
| {
op: "add_element";
key: string;
element: UIElement;
parentKey: string;
index?: number;
}
| { op: "remove_element"; key: string }
| { op: "update_props"; key: string; props: Record }
| { op: "set_prop"; key: string; prop: string; value: unknown }
| { op: "remove_prop"; key: string; prop: string }
| { op: "reorder_children"; key: string; children: string[] }
| { op: "set_root"; root: string }
| { op: "replace_spec"; spec: Spec }
| { op: "set_theme"; theme: Partial }
| { op: "set_theme_css"; css: string };
```
**Response (200):**
```json
{
"ok": true,
"revision": 6,
"applied": 3,
"errors": []
}
```
**curl:**
```bash
curl -X POST http://localhost:4321/api/sessions/a1b2c3d4-e5f6-7890-abcd-ef1234567890/ops \
-H "Content-Type: application/json" \
-d '{
"ops": [
{
"op": "add_element",
"key": "submitBtn",
"parentKey": "form",
"element": {
"type": "Button",
"props": { "children": "Submit", "className": "w-full" }
}
},
{
"op": "set_prop",
"key": "heading",
"prop": "children",
"value": "Welcome Back"
}
],
"agentId": "claude"
}'
```
---
## POST /api/sessions/:id/presence
Update agent presence for a session. Presence is stored in memory and visible to connected browsers.
**Request body:**
| Field | Type | Required | Description |
| --------- | -------- | -------- | ----------------------------------------- |
| `agentId` | `string` | Yes | Unique agent identifier |
| `name` | `string` | No | Display name |
| `cursor` | `object` | No | `{ elementKey: string }` — cursor target |
| `status` | `string` | No | Agent status (e.g. `"working"`, `"idle"`) |
**Response (200):**
```json
{
"ok": true,
"presence": {
"claude": {
"agentId": "claude",
"status": "working",
"updatedAt": "2026-03-25T10:05:00.000Z"
}
}
}
```
**curl:**
```bash
curl -X POST http://localhost:4321/api/sessions/a1b2c3d4-e5f6-7890-abcd-ef1234567890/presence \
-H "Content-Type: application/json" \
-d '{
"agentId": "claude",
"status": "working",
"name": "Claude"
}'
```
---
## DELETE /api/sessions/:id
Delete a session and its persisted Yjs document. Clears presence data.
**Response (200):**
```json
{ "ok": true }
```
**Response (404):**
```json
{ "error": "Session not found" }
```
**curl:**
```bash
curl -X DELETE http://localhost:4321/api/sessions/a1b2c3d4-e5f6-7890-abcd-ef1234567890
```
---
## WebSocket: /ws
The `/ws` endpoint accepts WebSocket connections for Yjs CRDT sync via the [Hocuspocus](https://tiptap.dev/hocuspocus) protocol.
### Connection
Connect with a Yjs-compatible WebSocket provider. The session ID is the Hocuspocus document name:
```typescript
import { HocuspocusProvider } from "@hocuspocus/provider";
import * as Y from "yjs";
const doc = new Y.Doc();
const provider = new HocuspocusProvider({
url: "ws://localhost:4321/ws",
name: sessionId, // the session ID is the document name
document: doc,
});
```
### Document Structure
The Yjs document contains these top-level maps:
| Map | Type | Description |
| ---------- | -------- | ------------------------------------- |
| `spec` | `Y.Map` | Compose spec with nested elements |
| `theme` | `Y.Map` | Theme parameters |
| `themeCSS` | `Y.Text` | Generated CSS token string |
| `meta` | `Y.Map` | Session metadata (id, name, revision) |
### Reading the Spec
```typescript
const specMap = doc.getMap("spec");
const root = specMap.get("root") as string;
const elements = specMap.get("elements") as Y.Map>;
// Observe changes
specMap.observeDeep((events) => {
// Re-render when spec changes
const spec = yDocToSpec(doc);
renderPreview(spec);
});
```
### Awareness Protocol
Presence and cursors use the Yjs [Awareness](https://docs.yjs.dev/api/about-awareness) protocol, carried over the same WebSocket connection:
```typescript
provider.awareness.setLocalState({
agentId: "claude",
status: "working",
color: "#7c3aed",
});
provider.awareness.on("change", () => {
const states = provider.awareness.getStates();
// Render presence indicators
});
```
---
## Type Reference
### Spec
```typescript
interface Spec {
root: string;
elements: Record;
state?: Record;
}
```
### UIElement
```typescript
interface UIElement {
type: string;
props: Record;
children?: string[];
}
```
### ThemeParams
```typescript
interface ThemeParams {
hue: number; // 0-360
chroma: number; // 0-0.4
grayChroma: number; // 0-0.03
radius: number; // px
font?: string;
}
```
### SessionMeta
```typescript
interface SessionMeta {
id: string;
name: string;
description: string;
revision: number;
createdAt: string; // ISO 8601
updatedAt: string; // ISO 8601
}
```
# Getting Started with Live Canvas
> Set up the Live Canvas and create your first live design session in under 5 minutes.
URL: https://prototyper-ui.com/docs/design-bridge/getting-started
## Prerequisites
- [Node.js](https://nodejs.org) 18+ for `npx`
- An MCP-compatible AI client (Claude Code, Cursor, OpenCode, VS Code, or Zed)
## Configure MCP
Add the Prototyper UI MCP server to your AI client. The hosted bridge is used by default; set `PROTOTYPER_BRIDGE_URL` only when you want a local or custom bridge.
### Claude Code
```bash
claude mcp add prototyper-ui -- npx -y @prototyperco/mcp@latest
```
Or add to `.mcp.json`:
```json
{
"mcpServers": {
"prototyper-ui": {
"command": "npx",
"args": ["-y", "@prototyperco/mcp@latest"]
}
}
}
```
### Cursor
Add to `.cursor/mcp.json`:
```json
{
"mcpServers": {
"prototyper-ui": {
"command": "npx",
"args": ["-y", "@prototyperco/mcp@latest"]
}
}
}
```
### VS Code
Add to `.vscode/mcp.json`:
```json
{
"servers": {
"prototyper-ui": {
"type": "stdio",
"command": "npx",
"args": ["-y", "@prototyperco/mcp@latest"]
}
}
}
```
## Create Your First Session
Ask your AI assistant to create a design. The assistant will call `design_create` automatically:
```
Build me a signup form with name, email, and password fields
```
The tool returns a session ID and preview URL:
```json
{
"sessionId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"previewUrl": "https://prototyper-ui.com/design?session=a1b2c3d4-e5f6-7890-abcd-ef1234567890&api=https%3A%2F%2Faa-prototyper-bridge.fly.dev",
"revision": 0,
"source": "bridge"
}
```
## Open the Preview
Open the `previewUrl` in your browser. You will see the initial design rendered with Prototyper UI components.
The browser connects to the bridge server over WebSocket. Every change the AI agent makes from this point forward appears live — no refresh needed.
## Watch It Build
As you continue talking to the AI, it calls `design_update` to add elements, change props, and restructure the layout. Each operation is applied to the shared Yjs document and synced to your browser in real time.
```
Add a "Remember me" checkbox below the password field.
Make the submit button full width.
Change the theme to warm sunset colors.
```
Each instruction triggers one or more MCP tool calls:
- `design_update` adds the checkbox and modifies the button
- `design_theme` updates the color tokens
## Customize the Theme
You can describe the theme in natural language:
```
Make it dark blue with sharp corners and Inter font
```
Or be precise with numeric values:
```
Set the theme to hue 220, chroma 0.3, radius 4, font "Inter"
```
The `design_theme` tool accepts both styles. Natural language descriptions are parsed into theme parameters automatically. Presets like "forest", "sunset", and "midnight" are also recognized.
## Export the Design
When the design looks right, export it as a React component:
```
Export this design as a React component
```
The AI calls `design_export`, which returns:
- A React component file using Prototyper UI imports
- Theme CSS tokens to add to your `globals.css`
- A list of component packages to install
## Close the Session
```
Close the design session
```
The AI calls `design_close`. Add `exportFirst: true` to export automatically before closing.
## Environment Variables
| Variable | Default | Description |
| ----------------------------- | -------------------------------------- | -------------------------- |
| `PROTOTYPER_BRIDGE_URL` | `https://aa-prototyper-bridge.fly.dev` | Bridge server URL |
| `PROTOTYPER_PREVIEW_BASE_URL` | `https://prototyper-ui.com` | Base URL for preview links |
## Next Steps
- [MCP Tools Reference](/docs/design-bridge/mcp-tools) — Full parameter docs for every design tool
- [API Reference](/docs/design-bridge/api-reference) — REST and WebSocket protocol details
# MCP Tools Reference
> Complete reference for the 7 Live Canvas MCP tools — create, update, theme, get, list, close, and export.
URL: https://prototyper-ui.com/docs/design-bridge/mcp-tools
The Live Canvas adds 7 MCP tools to the Prototyper UI server. All tools are available when the MCP server is running. If the canvas server is reachable, tools use the Yjs-backed canvas for real-time sync. Otherwise, they fall back to file-based sessions.
## design_create
Create a new live design session. Returns a session ID and preview URL.
### Parameters
| Parameter | Type | Required | Description |
| ------------- | -------- | -------- | ---------------------------------------------- |
| `name` | `string` | Yes | Session name, e.g. `"Login Page"` |
| `spec` | `string` | No | JSON Compose spec string (`root` + `elements`) |
| `description` | `string` | No | Human description of the design |
| `theme` | `object` | No | Theme overrides (merged with defaults) |
The `theme` object accepts:
| Property | Type | Range | Description |
| ------------ | -------- | ------ | -------------------- |
| `hue` | `number` | 0-360 | Primary color hue |
| `chroma` | `number` | 0-0.4 | Color saturation |
| `grayChroma` | `number` | 0-0.03 | Gray tint saturation |
| `radius` | `number` | 0+ | Border radius in px |
| `font` | `string` | -- | Font family name |
### Example
```json
{
"name": "Signup Form",
"description": "A registration form with name, email, and password",
"theme": {
"hue": 220,
"radius": 8
}
}
```
### Response
```json
{
"sessionId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"previewUrl": "https://prototyper-ui.com/design?session=a1b2c3d4&api=https%3A%2F%2Faa-prototyper-bridge.fly.dev",
"revision": 0,
"source": "bridge"
}
```
---
## design_update
Update the spec of an active design session. Accepts either a full replacement spec or RFC 6902 JSON Patch operations.
### Parameters
| Parameter | Type | Required | Description |
| ------------- | -------- | -------- | ------------------------------------------------ |
| `sessionId` | `string` | No | Session ID (falls back to active session) |
| `spec` | `string` | No | Full replacement spec as JSON string |
| `patches` | `string` | No | JSON Patch array (RFC 6902) as JSON string |
| `description` | `string` | No | Change description, e.g. `"Added submit button"` |
Provide exactly one of `spec` or `patches`, not both.
### Patches Format
Patches follow [RFC 6902](https://datatracker.ietf.org/doc/html/rfc6902). Common operations:
**Add an element:**
```json
[
{
"op": "add",
"path": "/elements/submitBtn",
"value": {
"type": "Button",
"props": { "children": "Submit", "className": "w-full" },
"children": []
}
}
]
```
**Change a prop:**
```json
[
{
"op": "replace",
"path": "/elements/submitBtn/props/children",
"value": "Sign Up"
}
]
```
**Remove an element:**
```json
[
{
"op": "remove",
"path": "/elements/submitBtn"
}
]
```
**Reorder children:**
```json
[
{
"op": "replace",
"path": "/elements/form/children",
"value": ["nameField", "emailField", "passwordField", "submitBtn"]
}
]
```
### Spec Operations
When connected to the bridge server, patches are converted to granular `SpecOp` operations for optimal CRDT merging:
| Operation | Description |
| ------------------ | ----------------------------- |
| `add_element` | Add a new element with parent |
| `remove_element` | Remove an element by key |
| `update_props` | Merge props into an element |
| `set_prop` | Set a single prop value |
| `remove_prop` | Remove a single prop |
| `reorder_children` | Set the children array |
| `set_root` | Change the root element key |
| `replace_spec` | Replace the entire spec |
### Response
```json
{
"revision": 3,
"applied": 2,
"errors": [],
"source": "bridge"
}
```
---
## design_theme
Update the theme of a design session. Accepts natural language descriptions, preset names, or explicit numeric parameters.
### Parameters
| Parameter | Type | Required | Description |
| ------------- | -------- | -------- | ------------------------------------------------------------------------ |
| `sessionId` | `string` | No | Session ID (falls back to active session) |
| `description` | `string` | No | Natural language, e.g. `"warm sunset colors with large rounded corners"` |
| `hue` | `number` | No | Primary hue (0-360) |
| `chroma` | `number` | No | Color saturation (0-0.4) |
| `grayChroma` | `number` | No | Gray tint saturation (0-0.03) |
| `radius` | `number` | No | Border radius in px |
| `font` | `string` | No | Font family name |
When both `description` and explicit parameters are provided, explicit parameters take precedence.
### Natural Language Examples
- `"dark blue professional"` — parsed to hue ~220, moderate chroma
- `"warm sunset with rounded corners"` — parsed to warm hues, large radius
- `"forest"` — resolved from the forest preset
- `"midnight"` — resolved from the midnight preset
### Example
```json
{
"description": "clean minimal with Inter font",
"radius": 6
}
```
### Response
```json
{
"theme": {
"hue": 240,
"chroma": 0.12,
"grayChroma": 0.005,
"radius": 6,
"font": "Inter"
},
"description": "Theme with hue 240, chroma 0.12, radius 6px, font \"Inter\"",
"source": "bridge"
}
```
---
## design_get
Get details about a design session at varying levels of detail.
### Parameters
| Parameter | Type | Required | Default | Description |
| ----------- | -------- | -------- | ----------- | ------------------------- |
| `sessionId` | `string` | No | active | Session ID |
| `include` | `enum` | No | `"summary"` | Level of detail to return |
The `include` parameter accepts:
| Value | Returns |
| ------------ | ----------------------------------------------------- |
| `summary` | ID, name, description, revision, element names, theme |
| `full` | Everything including full spec, theme CSS, history |
| `spec-only` | Just the spec |
| `theme-only` | Just the theme parameters and generated CSS |
### Example
```json
{
"include": "summary"
}
```
### Response (summary)
```json
{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"name": "Login Page",
"description": "A login form with email and password",
"revision": 5,
"componentCount": 8,
"elementNames": [
"container",
"heading",
"emailField",
"passwordField",
"submitBtn"
],
"theme": {
"hue": 264,
"chroma": 0.24,
"grayChroma": 0.01,
"radius": 0.625
},
"source": "bridge"
}
```
---
## design_list
List all active design sessions with metadata.
### Parameters
None.
### Response
```json
{
"sessions": [
{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"name": "Login Page",
"revision": 5
},
{
"id": "f9e8d7c6-b5a4-3210-fedc-ba9876543210",
"name": "Dashboard",
"revision": 12
}
],
"source": "bridge"
}
```
---
## design_close
Close a design session. Optionally export the design before closing.
### Parameters
| Parameter | Type | Required | Default | Description |
| ------------- | --------- | -------- | ------- | ----------------------------------- |
| `sessionId` | `string` | No | active | Session ID |
| `exportFirst` | `boolean` | No | `false` | Export as React code before closing |
### Example
```json
{
"exportFirst": true
}
```
### Response
```json
{
"closed": true,
"exported": {
"code": "import { Card, CardHeader, ... } from \"@/components/proto\";\n\nexport function LoginPage() { ... }",
"themeCSS": ":root { --primary: ... }"
},
"source": "bridge"
}
```
When `exportFirst` is `false`, the `exported` field is omitted.
---
## design_export
Export a design session as a React component with optional theme CSS.
### Parameters
| Parameter | Type | Required | Default | Description |
| --------------- | --------- | -------- | -------------------- | --------------------------------- |
| `sessionId` | `string` | No | active | Session ID |
| `componentName` | `string` | No | Derived from name | PascalCase component name |
| `importPrefix` | `string` | No | `@/components/proto` | Import path prefix for components |
| `includeTheme` | `boolean` | No | `true` | Include theme CSS in export |
### Example
```json
{
"componentName": "SignupForm",
"importPrefix": "@/components/ui"
}
```
### Response
```json
{
"code": "import { Card, CardHeader, CardContent, ... } from \"@/components/ui\";\n\nexport function SignupForm() {\n return (\n \n ...\n \n );\n}",
"themeCSS": ":root {\n --primary: 39.11% 0.084 240.8;\n ...\n}",
"installDeps": [
"@prototyperco/ui/card",
"@prototyperco/ui/button",
"@prototyperco/ui/text-field"
],
"files": [
{
"path": "components/signup-form.tsx",
"content": "..."
},
{
"path": "styles/theme-tokens.css",
"content": "..."
}
],
"source": "bridge"
}
```
# Design Principles
> Core principles that guide Prototyper UI's design and development.
URL: https://prototyper-ui.com/docs/design-principles
Prototyper UI follows a set of core principles that prioritize ownership, beauty, consistency, and developer experience. These principles shape every decision — from token naming to component API design.
## 1. You Own The Code
Components are files you copy into your project, read, edit, and extend. There is no hidden abstraction layer, no runtime CSS-in-JS, no provider you must wrap your app in. You install a component, it lands in your codebase, and it's yours.
This is the [shadcn philosophy](https://ui.shadcn.com/docs): the component library is a starting point, not a dependency.
```bash
# Install a component — it copies source into your project
npx shadcn@latest add https://prototyper-ui.com/r/button.json
```
```tsx
// You get a real file you can edit
// registry/ui/button.tsx
function Button({ className, variant, size, ...props }) {
return (
);
}
```
**Why this matters:** No version lock-in. No waiting for upstream fixes. No fighting abstraction layers to customize behavior. You change the file, you change the component.
## 2. Base UI Native
Every interactive component is built on [Base UI](https://base-ui.com) — the unstyled primitive library from the Material UI team. Base UI handles the hard problems (focus management, keyboard navigation, ARIA attributes, scroll locking, portal rendering) so we focus entirely on design.
```tsx
// Base UI provides the behavior, we provide the style
import { Select as SelectPrimitive } from "@base-ui/react/select";
function SelectTrigger({ className, children, ...props }) {
return (
{children}
} />
);
}
```
**What Base UI gives us for free:**
- WCAG 2.1 AA compliance across all components
- Keyboard navigation (arrow keys, type-ahead, focus trapping)
- Screen reader announcements via ARIA attributes
- Data attributes (`data-open`, `data-disabled`, `data-highlighted`) as a public styling contract
- Portal rendering for overlays
- Scroll lock for modals
## 3. Beautiful by Default
Components should look exceptional without any customization. The design system uses:
- **OKLCH color space** — perceptually uniform, `color-mix()` works predictably for derived states
- **Multi-layer shadows** — three stacked shadow layers simulate realistic light (subtle edge shadow is critical for dark mode)
- **Role-based surfaces** — surfaces, overlays, and fields each have dedicated tokens and shadow tiers
- **Fluid easing** — `cubic-bezier(0.32, 0.72, 0, 1)` as the signature curve (Apple-style deceleration)
- **Gradient buttons** — the default button uses a three-layer gradient system with primary color ramps
Beauty is not decoration — it's the result of precise tokens, consistent spacing, and careful shadow/color relationships.
## 4. Role-Based Surfaces
Inspired by [HeroUI v3's surface system](https://v3.heroui.com), surfaces are categorized by their **role in the UI**, not by arbitrary elevation numbers:
| Role | Token | Shadow | Components |
| ----------- | --------------------- | ---------------- | ----------------------------------- |
| **Surface** | `bg-surface` | `shadow-surface` | Cards, panels, tabs |
| **Overlay** | `bg-overlay` | `shadow-overlay` | Dialogs, popovers, menus, dropdowns |
| **Field** | `bg-field-background` | `shadow-field` | Inputs, selects, comboboxes |
**Light mode** uses shadows for depth. **Dark mode** uses background lightness stepping — shadows are reduced or zeroed because dark surfaces already show depth through tonal difference.
Three surface tiers handle nesting (card-in-card):
- `bg-surface` — primary surface
- `bg-surface-secondary` — nested containers (derived via `color-mix`)
- `bg-surface-tertiary` — deeper nesting (derived via `color-mix`)
## 5. CSS Utilities for Consistency
Every component references shared CSS utilities instead of reimplementing focus, disabled, and invalid patterns. This guarantees identical behavior everywhere:
```css
/* Focus ring — buttons, links, interactive elements */
@utility focus-ring {
outline: 2px solid var(--color-ring);
outline-offset: 2px;
}
/* Focus ring — form fields (sits on border, no offset) */
@utility focus-field-ring {
outline: 2px solid var(--color-ring);
outline-offset: -1px;
}
/* Disabled state — universal across all components */
@utility status-disabled {
opacity: 0.5;
pointer-events: none;
cursor: not-allowed;
}
```
```tsx
// Every component uses the same utilities
```
**Why not per-component styles?** Because 5 different focus ring implementations (different widths, colors, offsets) is how inconsistency creeps in. One utility, one look, everywhere.
## 6. Flat Exports, shadcn-Compatible
Components use flat named exports — no `Object.assign` compound patterns, no dot notation. This is what shadcn users know and what LLMs generate best.
```tsx
// Named exports — clear, greppable, tree-shakeable
import {
Select,
SelectTrigger,
SelectContent,
SelectItem,
SelectValue,
} from "@/components/ui/select";
Option A
Option B
;
```
Every component root and sub-component has a `data-slot` attribute for external targeting:
```css
/* Target any component part from outside */
[data-slot="select-trigger"] {
/* ... */
}
[data-slot="select-content"] {
/* ... */
}
```
## 7. Progressive Disclosure
Components work with minimal props and scale up as requirements grow. The simplest usage should be a single line; advanced usage reveals more knobs.
```tsx
// Level 1: Minimal — just works
// Level 2: Control what's visible
// Level 3: Full composition
```
## 8. CSS-First Animation
No JavaScript animation libraries. All transitions and animations use CSS, data attributes for state, and `prefers-reduced-motion` support.
```css
/* Overlay enter/exit via Tailwind animate utilities */
data-open:animate-in data-closed:animate-out
data-closed:fade-out-0 data-open:fade-in-0
data-closed:zoom-out-95 data-open:zoom-in-95
/* Interactive elements use transition-colors */
transition-colors duration-200
/* Easing tokens from the design system */
--ease-out-fluid: cubic-bezier(0.32, 0.72, 0, 1);
--ease-smooth: cubic-bezier(0.4, 0, 0.2, 1);
```
**Reduced motion is respected automatically.** All components use Tailwind's `motion-reduce:` variant which maps to both `prefers-reduced-motion` media query and `data-reduce-motion` attribute.
## 9. Type Safety
Full TypeScript with strict mode. Component props are derived from Base UI's type definitions, extended where needed:
```tsx
// Props extend Base UI types — full IntelliSense
function SelectTrigger({
className,
size = "default",
children,
...props
}: SelectPrimitive.Trigger.Props & {
size?: "sm" | "default" | "lg";
}) {
// ...
}
```
Variant types are generated by `class-variance-authority`:
```tsx
import { cva, type VariantProps } from "class-variance-authority";
const buttonVariants = cva("...", {
variants: {
variant: { default: "...", destructive: "...", outline: "..." },
size: { default: "h-9", sm: "h-8", lg: "h-10" },
},
});
// VariantProps gives you { variant?: "default" | ... }
```
## 10. LLM-Friendly
Prototyper UI is designed to be generated correctly by AI. Every component follows the same patterns:
- **Consistent file structure:** `"use client"`, imports, component functions, exports
- **Consistent naming:** `ComponentName` + `ComponentNameSubPart` (e.g., `Select`, `SelectTrigger`, `SelectContent`)
- **Consistent props:** `className` + `...props` spread on every component
- **Consistent styling:** Tailwind utilities, `cn()` for merging, `data-slot` for identification
- **Rich examples:** Every component has multiple examples showing common patterns
- **Structured docs:** MDX pages with live previews, installation commands, and usage snippets
When an LLM reads one component, it understands all of them.
---
## Design Token Architecture
### Color Space: OKLCH
All colors use the OKLCH color space with `color-mix(in oklab)` for derived states:
```css
/* Base token (manually defined per theme) */
--primary: 39.11% 0.084 240.8;
/* Derived hover state (90% base + 10% foreground) */
--primary-hover: color-mix(
in oklab,
oklch(var(--primary)) 90%,
oklch(var(--primary-foreground)) 10%
);
/* Derived soft variant (15% opacity) */
--primary-soft: color-mix(in oklab, oklch(var(--primary)) 15%, transparent);
```
**Why OKLCH?** Perceptually uniform — a 10% mix shift looks like 10% regardless of the base color. HSL-based mixing produces unpredictable results across hues.
### Shadow System
Three semantic shadow tiers, mode-adaptive:
| Tier | Light Mode | Dark Mode | Used For |
| ---------------- | ------------------------- | ----------------------------- | ------------------------ |
| `shadow-surface` | Multi-layer subtle shadow | None (tonal contrast instead) | Cards, panels |
| `shadow-field` | Subtle shadow + 1px edge | None | Form inputs |
| `shadow-overlay` | Heavy multi-layer shadow | Subtle 1px inset white glow | Popovers, menus, dialogs |
### What We Derive vs. Define Manually
| Manually defined | Derived via `color-mix()` |
| ------------------------------------------------ | ---------------------------------------- |
| Base colors (`--primary`, `--destructive`, etc.) | Hover states (90% base + 10% foreground) |
| Foreground colors (`--primary-foreground`, etc.) | Soft variants (15% base + transparent) |
| Color ramps (`--primary-light/middle/dark`) | Surface tiers (secondary, tertiary) |
| Surface/overlay/field backgrounds | Border/separator progressions |
**Why not derive everything?** Tested with 5 brand colors — ramp derivation collapses for light colors, washes chroma for dark colors. Foreground can't be auto-derived (green needs dark text in both modes). Manual definition where it matters, derivation where it's safe.
---
## Comparison with Other Libraries
| Aspect | shadcn/ui | HeroUI v3 | Prototyper UI |
| --------------------- | --------------------- | ---------------------------------- | ---------------------------------- |
| **Primitive library** | Radix UI | React Aria | Base UI |
| **Ownership model** | Copy-paste | Package dependency | Copy-paste |
| **Color space** | HSL | OKLCH | OKLCH |
| **Surface system** | Ad-hoc (3 tokens) | Role-based (surface/overlay/field) | Role-based (surface/overlay/field) |
| **Shadow system** | Size-based (sm/md/lg) | Semantic (surface/overlay) | Semantic (surface/field/overlay) |
| **Animation** | CSS + Tailwind | CSS + GPU accelerated | CSS + Tailwind |
| **Component API** | Flat exports | Compound (dot notation) | Flat exports |
| **Styling** | Tailwind-in-component | BEM + CSS layers | Tailwind-in-component |
| **Dark mode shadows** | Same as light | Zeroed / inset glow | Zeroed / inset glow |
# LLMs.txt
> Machine-readable documentation endpoints for AI assistants
URL: https://prototyper-ui.com/docs/for-agents/llms-txt
Prototyper UI exposes several plain-text endpoints optimized for LLM consumption, following the [llms.txt convention](https://llmstxt.org).
These endpoints exist because rich HTML docs are not the friendliest format for a language model. Stripping the navigation, sidebars, JavaScript, and visual chrome leaves the model with the parts it actually uses: prose, code examples, and TypeScript signatures. Pointing your AI assistant at `/llms-full.txt` or `/llms-components.txt` gives it an accurate, low-token snapshot of the entire library, so suggestions stay grounded in the real API instead of drifting toward best-guess pattern matching. The endpoints are regenerated on every build, so the model always has the current version of the docs and component source — no manual sync step required.
## Endpoints
| URL | Purpose |
| -------------------------------------------------- | ------------------------------------------------------------------------------------------------- |
| [`/llms.txt`](/llms.txt) | Index of all docs pages and components |
| [`/llms-full.txt`](/llms-full.txt) | Complete documentation — all pages in one file |
| [`/llms-components.txt`](/llms-components.txt) | All components with full TypeScript source and examples |
| `/llms/{slug}` | Individual page LLM text (e.g. [`/llms/button`](/llms/button)) |
| `/llms/components/{name}` | Component docs + source via full path (e.g. [`/llms/components/button`](/llms/components/button)) |
| [`/prototyper-tokens.css`](/prototyper-tokens.css) | Complete OKLCH design tokens as a downloadable CSS file |
The `/llms/{slug}` endpoint is a catch-all: any docs page slug works, not just component names.
## Usage
**Claude Code — reference in conversation:**
```
@https://prototyper-ui.com/llms.txt
```
**Claude Code — add to CLAUDE.md:**
```markdown
## UI Library
See @https://prototyper-ui.com/llms.txt for available components.
```
**Cursor — add to `.cursorrules`:**
```
@Docs https://prototyper-ui.com/llms-full.txt
```
**OpenCode — add to rules file:**
```
#docs https://prototyper-ui.com/llms-full.txt
```
**Direct fetch:**
```bash
curl https://prototyper-ui.com/llms/button
curl https://prototyper-ui.com/llms-components.txt
```
## Response Format
All endpoints return `text/plain; charset=utf-8`. Component endpoints include:
- Frontmatter (title, description, docs URL, Base UI reference link)
- Full MDX documentation converted to plain markdown
- All `` examples inlined as `tsx` code blocks
- Full TypeScript component source
- Additional examples not already shown in docs
# MCP Server
> Give your AI assistant structured access to Prototyper UI components via the Model Context Protocol
URL: https://prototyper-ui.com/docs/for-agents/mcp-server
The `@prototyperco/mcp` package provides structured access to components, docs, and design tokens via the [Model Context Protocol](https://modelcontextprotocol.io).
## Setup
### Claude Code
```bash
claude mcp add prototyper-ui -- npx -y @prototyperco/mcp@latest
```
Or add to `.claude/settings.local.json` in your project:
```json
{
"mcpServers": {
"prototyper-ui": {
"command": "npx",
"args": ["-y", "@prototyperco/mcp@latest"]
}
}
}
```
### Cursor
Add to `.cursor/mcp.json`:
```json
{
"mcpServers": {
"prototyper-ui": {
"command": "npx",
"args": ["-y", "@prototyperco/mcp@latest"]
}
}
}
```
### OpenCode
Add to your MCP config (e.g. `.opencode/mcp.json`):
```json
{
"mcpServers": {
"prototyper-ui": {
"command": "npx",
"args": ["-y", "@prototyperco/mcp@latest"]
}
}
}
```
### VS Code
Add to `.vscode/settings.json`:
```json
{
"mcp.servers": {
"prototyper-ui": {
"command": "npx",
"args": ["-y", "@prototyperco/mcp@latest"]
}
}
}
```
### Gemini CLI
Add to `.gemini/settings.json` in your project, or `~/.gemini/settings.json` globally:
```json
{
"mcpServers": {
"prototyper-ui": {
"command": "npx",
"args": ["-y", "@prototyperco/mcp@latest"]
}
}
}
```
### Zed
Add to `.zed/settings.json`:
```json
{
"context_servers": {
"prototyper-ui": {
"command": {
"path": "npx",
"args": ["-y", "@prototyperco/mcp@latest"]
}
}
}
}
```
## Available Tools
| Tool | Description |
| ---------------------- | ----------------------------------------------------- |
| `list_components` | List all available components with descriptions |
| `get_component_docs` | Full documentation, API reference, and usage examples |
| `get_component_source` | Full TypeScript source code for a component |
| `get_theme` | Complete OKLCH design tokens CSS |
| `search_docs` | Full-text search across all documentation |
## Resources
The server also exposes MCP resources for direct access:
| Resource URI | Content |
| ------------------------------------- | ----------------------- |
| `prototyper://tokens/css` | Full design tokens CSS |
| `prototyper://docs/components/{name}` | Component documentation |
## Environment Variables
| Variable | Default | Description |
| ------------------------ | --------------------------- | ---------------------------------------------------- |
| `PROTOTYPER_UI_BASE_URL` | `https://prototyper-ui.com` | Override the base URL (useful for local development) |
## Local Development
Point the MCP server at your local docs instance:
```bash
PROTOTYPER_UI_BASE_URL=http://localhost:3333 npx -y @prototyperco/mcp@latest
```
## Troubleshooting
- Use `@prototyperco/mcp`, not `@prototyperco/cli mcp`. The MCP package is the supported entrypoint and does not require Bun.
- Gemini CLI MCP setup and agent skills are separate. Adding a skill does not register an MCP server, and MCP config does not install skills.
- If Gemini CLI still shows the server as disconnected after editing `settings.json`, run `/mcp refresh` or restart the session.
# Agent Skills
> Install a Prototyper UI skill into Claude Code, Cursor, OpenCode, Gemini CLI, and Google Antigravity
URL: https://prototyper-ui.com/docs/for-agents/skills
Agent skills give AI assistants inline access to Prototyper UI patterns, component APIs, and examples — without needing to fetch from the web on every request.
## Install (all detected tools)
```bash
curl -fsSL https://prototyper-ui.com/install.sh | bash
```
This detects and installs to: **Claude Code**, **Cursor**, **OpenCode**, **Codex CLI**, **Gemini CLI**, and **Google Antigravity**.
## Manual install — Claude Code
```bash
claude skill add https://prototyper-ui.com/install.sh
```
Or using the install script with a custom name:
```bash
curl -fsSL https://prototyper-ui.com/install.sh | bash -s -- my-prototyper-ui
```
## Gemini CLI and Google Antigravity
The installer writes skills for Gemini CLI and Google Antigravity separately so both tools can discover the bundle.
- Gemini CLI: `~/.gemini/skills/prototyper-ui`
- Google Antigravity: `~/.gemini/antigravity/global_skills/prototyper-ui` and `~/.gemini/antigravity/skills/prototyper-ui`
Skills do not configure MCP. To add the Prototyper UI MCP server in Gemini CLI, update `.gemini/settings.json` with `npx -y @prototyperco/mcp@latest` and then run `/mcp refresh`.
## What's included
The skill bundle contains:
| File | Purpose |
| -------------------- | ---------------------------------------------------------------------- |
| `SKILL.md` | Main skill definition: component list, patterns, theming, and pitfalls |
| `scripts/list.mjs` | Fetch and display all available components |
| `scripts/docs.mjs` | Fetch full docs + source for one or more components |
| `scripts/source.mjs` | Fetch component source code only |
| `scripts/theme.mjs` | Fetch complete OKLCH design tokens |
## Usage after install
Once installed, run scripts from within the skill directory:
```bash
# List all components
node ~/.claude/skills/prototyper-ui/scripts/list.mjs
# Get button docs and source
node ~/.claude/skills/prototyper-ui/scripts/docs.mjs button
# Get multiple component docs at once
node ~/.claude/skills/prototyper-ui/scripts/docs.mjs button dialog select
# Get component source only
node ~/.claude/skills/prototyper-ui/scripts/source.mjs button
# Get design tokens
node ~/.claude/skills/prototyper-ui/scripts/theme.mjs
```
## Skill content
The `SKILL.md` covers:
- Why Prototyper UI uses `@base-ui/react` instead of Radix UI
- All 19 available components with descriptions
- Component patterns: `"use client"`, `data-slot`, CVA, `cn()`, compound exports
- OKLCH color system and design token categories
- CSS utility classes (`focus-ring`, `status-disabled`, etc.)
- Animation guidelines and easing values
- Common pitfalls and how to avoid them
- Installation and project setup
## Environment variable
Override the base URL to use a local docs server:
```bash
PROTOTYPER_UI_BASE_URL=http://localhost:3333 node scripts/docs.mjs button
```
# Forms
> Build accessible forms with validation using FormField, Field, TextField, and react-hook-form.
URL: https://prototyper-ui.com/docs/forms
Prototyper UI provides a composable form system built on Base UI's [Field](https://base-ui.com/react/components/field) primitive. For react-hook-form users, the [`FormField`](/docs/components/form-field) helper reduces boilerplate to a single component per field.
## Recommended: FormField
The fastest way to build validated forms. `FormField` wires `Controller`, `Field`, `FieldLabel`, `FieldDescription`, and `FieldError` into one component:
```tsx
"use client";
import { useForm, FormProvider } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { FormField } from "@/components/ui/form-field";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
const schema = z.object({
name: z.string().min(1, "Name is required"),
email: z.string().email("Invalid email"),
});
type FormValues = z.infer;
export function ContactForm() {
const form = useForm({
resolver: zodResolver(schema),
defaultValues: { name: "", email: "" },
});
return (
Submit
);
}
```
See the [FormField docs](/docs/components/form-field) for validation, dynamic fields, and multi-step examples.
---
## Advanced: Raw Field components
For full control or components with non-standard change handlers (Select, NumberField, Checkbox, Switch, RadioGroup), use `Field` directly with react-hook-form's `Controller`.
## Basic form
A simple form using `TextField` with `Field` components for structure:
```tsx
import { Button } from "@/components/ui/button";
import { Field, FieldLabel, FieldDescription } from "@/components/ui/field";
import { TextField, Input } from "@/components/ui/text-field";
export function ContactForm() {
return (
Name
Email
We'll never share your email.
Submit
);
}
```
## Field components
### Field
The `Field` wrapper connects labels, inputs, descriptions, and errors. It renders a `role="group"` container with `data-slot="field"`.
```tsx
import {
Field,
FieldLabel,
FieldDescription,
FieldError,
} from "@/components/ui/field";
Username
{/* your input here */}
Choose a unique username.
Username is already taken.
;
```
### TextField
Combines Base UI's `Field.Root` with an `Input` or `TextArea`. Provides built-in validation binding.
```tsx
import { TextField, Input, TextArea } from "@/components/ui/text-field";
{
/* Single-line input */
}
Name
;
{
/* Multi-line textarea */
}
Bio
;
```
### NumberField
A numeric input with increment/decrement buttons and keyboard support.
```tsx
import {
NumberField,
NumberFieldGroup,
NumberFieldInput,
NumberFieldSteppers,
} from "@/components/ui/number-field";
Quantity
;
```
### Select
A dropdown select with search, grouping, and keyboard navigation.
```tsx
import {
Select,
SelectTrigger,
SelectValue,
SelectContent,
SelectItem,
} from "@/components/ui/select";
Role
Admin
Editor
Viewer
;
```
## FieldSet and FieldLegend
Group related fields together with `FieldSet` for semantic form structure:
```tsx
import { FieldSet, FieldLegend } from "@/components/ui/field";
Personal Information
First name
Last name
;
```
## Horizontal layout
Use the `orientation` prop on `Field` for side-by-side label and input layouts:
```tsx
Email
```
Available orientations: `"vertical"` (default), `"horizontal"`, and `"responsive"` (vertical on small screens, horizontal on larger ones).
## Required fields
Mark a field as required using the `required` prop on `FieldLabel`:
```tsx
Email
```
This adds a red asterisk (`*`) after the label text.
## Showing errors
Use `FieldError` to display validation messages. The error animates in with a height transition:
```tsx
Email
Please enter a valid email address.
```
`FieldError` also accepts an `errors` array for showing multiple messages:
```tsx
```
## react-hook-form integration
### Setup
Install react-hook-form and zod:
```bash
npm install react-hook-form @hookform/resolvers zod
```
### Basic example
```tsx
"use client";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { Button } from "@/components/ui/button";
import { Field, FieldLabel, FieldError } from "@/components/ui/field";
import { TextField, Input } from "@/components/ui/text-field";
const schema = z.object({
email: z.string().email("Please enter a valid email"),
password: z.string().min(8, "Must be at least 8 characters"),
});
type FormValues = z.infer;
export function LoginForm() {
const {
register,
handleSubmit,
formState: { errors },
} = useForm({
resolver: zodResolver(schema),
});
const onSubmit = (data: FormValues) => {
console.log(data);
};
return (
Email
{errors.email?.message}
Password
{errors.password?.message}
Log in
);
}
```
### With all field types
```tsx
"use client";
import { useForm, Controller } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { Button } from "@/components/ui/button";
import {
Field,
FieldLabel,
FieldDescription,
FieldError,
FieldSet,
FieldLegend,
} from "@/components/ui/field";
import { TextField, Input, TextArea } from "@/components/ui/text-field";
import {
NumberField,
NumberFieldGroup,
NumberFieldInput,
NumberFieldSteppers,
} from "@/components/ui/number-field";
import {
Select,
SelectTrigger,
SelectValue,
SelectContent,
SelectItem,
} from "@/components/ui/select";
const schema = z.object({
name: z.string().min(1, "Name is required"),
email: z.string().email("Invalid email address"),
age: z.number().min(18, "Must be at least 18").max(120),
role: z.string().min(1, "Please select a role"),
bio: z.string().max(500, "Bio must be under 500 characters").optional(),
});
type FormValues = z.infer;
export function RegistrationForm() {
const {
register,
handleSubmit,
control,
formState: { errors, isSubmitting },
} = useForm({
resolver: zodResolver(schema),
});
const onSubmit = async (data: FormValues) => {
await fetch("/api/register", {
method: "POST",
body: JSON.stringify(data),
});
};
return (
Account Details
Name
{errors.name?.message}
Email
{errors.email?.message}
Profile
(
Age
{errors.age?.message}
)}
/>
(
Role
Developer
Designer
Manager
{errors.role?.message}
)}
/>
Bio
Optional — tell us about yourself.
{errors.bio?.message}
Create account
);
}
```
### Key patterns
- **TextField + register**: For text and textarea inputs, spread `{...register("fieldName")}` directly on ` ` or ``.
- **NumberField + Controller**: For `NumberField`, use react-hook-form's `Controller` because it uses `onValueChange` instead of `onChange`.
- **Select + Controller**: Same as NumberField — use `Controller` to bridge `onValueChange`.
- **invalid prop**: Pass `invalid={!!errors.fieldName}` to `TextField` or `NumberField` to trigger error styling on the input.
- **FieldError**: Render inside the field wrapper. It auto-animates with a height transition when content appears.
## Server-side validation
For server actions in Next.js, you can handle validation on the server and return errors to display in `FieldError`:
```tsx
"use client";
import { useActionState } from "react";
import { Button } from "@/components/ui/button";
import { Field, FieldLabel, FieldError } from "@/components/ui/field";
import { TextField, Input } from "@/components/ui/text-field";
interface FormState {
errors?: {
email?: string[];
password?: string[];
};
message?: string;
}
export function ServerForm({
action,
}: {
action: (state: FormState, formData: FormData) => Promise;
}) {
const [state, formAction, isPending] = useActionState(action, {});
return (
Email
{state.errors?.email?.[0]}
Password
{state.errors?.password?.[0]}
{state.message && (
{state.message}
)}
Submit
);
}
```
And the corresponding server action:
```tsx
"use server";
import { z } from "zod";
const schema = z.object({
email: z.string().email("Invalid email"),
password: z.string().min(8, "Must be at least 8 characters"),
});
export async function loginAction(state: FormState, formData: FormData) {
const result = schema.safeParse({
email: formData.get("email"),
password: formData.get("password"),
});
if (!result.success) {
return {
errors: result.error.flatten().fieldErrors,
};
}
// Authenticate user...
return { message: "Success" };
}
```
## Related components
- [Field](/docs/components/field) — Labels, descriptions, errors, and layout
- [TextField](/docs/components/text-field) — Text and textarea inputs with Base UI validation
- [NumberField](/docs/components/number-field) — Numeric input with increment/decrement
- [Select](/docs/components/select) — Dropdown selection
- [Checkbox](/docs/components/checkbox) — Boolean toggle
- [RadioGroup](/docs/components/radio-group) — Single selection from a group
- [Switch](/docs/components/switch) — Toggle switch
# Getting Started with Prototyper UI
> Set up Prototyper UI in your project and build your first page in under 5 minutes.
URL: https://prototyper-ui.com/docs/getting-started
## Prerequisites
Before you begin, make sure you have:
- [Bun](https://bun.sh) 1.1+ for the Prototyper UI CLI
- [Node.js](https://nodejs.org) 18 or later
- [React](https://react.dev) 19+
- [Next.js](https://nextjs.org) 15+ (or any React framework with Tailwind CSS v4)
- [Tailwind CSS](https://tailwindcss.com) v4
## Initialize your project
Run the `init` command to set up design tokens, utilities, and base styles:
```bash
bunx @prototyperco/cli init
```
Use `@prototyperco/cli` in the command. The unscoped `prototyper` npm package is unrelated and does not provide this CLI.
This will:
1. Merge OKLCH design tokens into your `globals.css`
2. Create `lib/utils.ts` with the `cn()` utility
3. Write a `prototyper.json` manifest
4. Optionally install AI skills for Claude Code or Cursor
## Add your first component
Install a component using the CLI:
```bash
bunx @prototyperco/cli add button
```
The component source file lands in `components/ui/button.tsx` — you own this file and can edit it freely.
Run `add` without arguments for an interactive picker, or install multiple components at once:
```bash
bunx @prototyperco/cli add button card dialog field text-field
```
To install every available component:
```bash
bunx @prototyperco/cli add --all
```
## Open the terminal app
After `init`, run the CLI without a subcommand to open the full-screen terminal app:
```bash
bunx @prototyperco/cli
```
Use `browse` when you want to jump straight into the component catalog:
```bash
bunx @prototyperco/cli browse
```
The terminal app needs Bun, a real TTY, color output, and a non-dumb terminal. In CI, piped output, `TERM=dumb`, or when `NO_COLOR` is set, the CLI falls back to picker or plain-text output.
## Install dependencies
If the CLI didn't install them automatically, add the required peer dependencies:
```bash
npm install @base-ui/react class-variance-authority clsx tailwind-merge
```
## Use a component
Import and use the component in your page:
```tsx
import { Button } from "@/components/ui/button";
export default function Page() {
return (
Default
Outline
Destructive
);
}
```
## Build a simple page
Here's a complete example combining several components:
```tsx
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Field, FieldLabel, FieldDescription } from "@/components/ui/field";
import { TextField, Input } from "@/components/ui/text-field";
export default function SignUpPage() {
return (
Create an account
Enter your details to get started.
Name
Email
Sign up
);
}
```
## Customize your theme
Prototyper UI uses OKLCH color tokens defined in your `globals.css`. To customize colors, edit the CSS custom properties in the `:root` block:
```css
:root {
--primary: 39.11% 0.084 240.8; /* Lightness Chroma Hue */
--primary-foreground: 98.48% 0.002 247.84;
}
```
For a visual editor, use the [Theme Builder](/design) to pick colors and copy the generated tokens.
See the [Theming](/docs/theming) guide for the full token reference.
## Verify your setup
Run the doctor command to check that everything is configured correctly:
```bash
bunx @prototyperco/cli doctor
```
## Using with shadcn
If you already use shadcn, you can install Prototyper UI components directly from the registry without running `init`:
```bash
npx shadcn@latest add https://prototyper-ui.com/r/button.json
```
Both libraries can coexist — Prototyper UI components install to the same `components/ui/` directory and follow the same conventions.
## Accessibility notes
### Touch targets
Prototyper UI components meet accessibility standards out of the box. However, icon-only buttons default to `size-8` (32px), which is below the [WCAG 2.5.8](https://www.w3.org/WAI/WCAG22/Understanding/target-size-minimum.html) recommended minimum of 44px.
For touch-heavy interfaces (mobile apps, kiosks), use `size="sm"` or add custom padding to increase the hit area:
```tsx
{
/* Option 1: Use a larger size */
}
;
{
/* Option 2: Add padding to the icon button */
}
;
```
## Next steps
- Browse all [Components](/docs/components/button) to see what's available
- Read the [CLI Reference](/docs/cli) for all commands and options
- Learn about [Forms](/docs/forms) with validation and react-hook-form
- Explore the [Theming](/docs/theming) guide for deep customization
- Try the [Theme Builder](/design) for visual token editing
# Introduction
> A composable React component library built on Base UI, Tailwind CSS v4, and copy-paste source ownership.
URL: https://prototyper-ui.com/docs/index
## What is Prototyper UI?
Prototyper UI is a collection of beautifully designed, accessible components built on [Base UI](https://base-ui.com) and styled with [Tailwind CSS v4](https://tailwindcss.com). Components are copied into your project as source files, so you own and control every line of code.
The library is aimed at teams who want production-quality React components without taking on a runtime dependency or fighting an opinionated abstraction layer. Every component is a real file in your repository — you can read it, edit it, restyle it, fork it, or delete it. There is no breaking-change migration to fear and no opaque package to debug. If a component does ninety percent of what you need, the remaining ten percent is a normal pull request rather than a feature request to an upstream maintainer.
Install any component with a single command:
```bash
npx shadcn@latest add https://prototyper-ui.com/r/button.json
```
The component lands in your codebase as a real file you can read, edit, and extend. No runtime dependency, no version lock-in, no abstraction layer to fight.
### Features
- Built on Base UI for accessible, unstyled primitives
- Styled with Tailwind CSS v4
- Dark mode support
- Themeable with CSS variables
- shadcn-compatible registry
- LLM-friendly source, catalog, and documentation endpoints
### Dependencies
Components are built with:
- [Base UI](https://base-ui.com) — Unstyled UI primitives
- [Tailwind CSS v4](https://tailwindcss.com) — Utility-first CSS
- [class-variance-authority](https://cva.style) — Variant management
- [tailwind-merge](https://github.com/dcastil/tailwind-merge) — Class merging
### Where to go next
Start with [Getting Started with Prototyper UI](/docs/getting-started) to install the registry and add your first component. From there, the [Components](/docs/components) section catalogues every primitive, each with a live preview, copy-paste install command, accessibility notes, and a full API reference. If you want a deeper customization story, the [Themes](/docs/themes) guide walks through the OKLCH token system and `color-mix()` derivations that power dark mode and theme switching. For AI-assisted workflows, browse [LLMs.txt](/docs/for-agents/llms-txt) for machine-readable endpoints that plug straight into Claude, Cursor, and similar tools.
# Installation
> How to set up Prototyper UI in your Next.js project with Tailwind CSS v4 and Base UI.
URL: https://prototyper-ui.com/docs/installation
## Requirements
Prototyper UI components require the following:
- [Bun](https://bun.sh) 1.1+ for the Prototyper UI CLI
- [React](https://react.dev) 19+
- [Next.js](https://nextjs.org) 15+
- [Tailwind CSS](https://tailwindcss.com) v4
- [Base UI](https://base-ui.com) (`@base-ui/react`)
## Quick Start
### Option A: Prototyper UI CLI (Recommended)
Set up your project and add components with the Prototyper UI CLI:
```bash
bunx @prototyperco/cli init # tokens, utils, base styles
bunx @prototyperco/cli add button # add components
```
Use the scoped package name exactly as shown. `bunx prototyper` resolves to an unrelated npm package and does not run the Prototyper UI CLI.
After `init`, run the CLI without a subcommand for the full-screen terminal app, or jump straight to the component catalog:
```bash
bunx @prototyperco/cli
bunx @prototyperco/cli browse
```
Run `add` without arguments for an interactive component picker, or install everything at once:
```bash
bunx @prototyperco/cli add --all
```
### Machine Mode Package (Installable Runtime)
To scaffold URL-driven machine mode in a Next.js App Router project:
```bash
bunx @prototyperco/cli machine-mode init
```
See the full guide at [Machine Mode Package](/docs/machine-mode).
### Option B: shadcn CLI
If you already use shadcn, install any component directly from the registry:
```bash
npx shadcn@latest add https://prototyper-ui.com/r/button.json
```
This copies the component source into your project (typically `components/ui/button.tsx`) along with any required dependencies.
```bash
npx shadcn@latest add https://prototyper-ui.com/r/button.json https://prototyper-ui.com/r/dialog.json https://prototyper-ui.com/r/select.json
```
## Keeping Up to Date
```bash
bunx @prototyperco/cli update # check for component updates
bunx @prototyperco/cli doctor # verify project setup
```
## Manual Setup
If you prefer to set up manually or need to understand each step, follow this guide.
### 1. Install Dependencies
```bash
npm install @base-ui/react class-variance-authority clsx tailwind-merge
```
### 2. Add the `cn` Utility
Create `lib/utils.ts` in your project:
```ts
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
```
### 3. Set Up CSS Tokens
Add the design tokens to your `globals.css`. The token system has four layers:
**Theme registration** — maps CSS custom properties to Tailwind utilities:
```css
@theme {
/* Radius scale */
--radius-xl: calc(var(--radius) + 4px);
--radius-lg: var(--radius);
--radius-md: calc(var(--radius) - 2px);
--radius-sm: calc(var(--radius) - 4px);
/* Colors — each generates bg-*, text-*, border-* utilities */
--color-background: oklch(var(--background));
--color-foreground: oklch(var(--foreground));
--color-primary: oklch(var(--primary));
--color-primary-foreground: oklch(var(--primary-foreground));
/* ... see full token list in the Theming guide */
/* Shadows — three semantic tiers */
--shadow-surface:
0 2px 4px 0 oklch(0% 0 0 / 0.04), 0 1px 2px 0 oklch(0% 0 0 / 0.06),
0 0 0 1px oklch(0% 0 0 / 0.04);
--shadow-field:
0 1px 2px 0 oklch(0% 0 0 / 0.05), 0 0 0 1px oklch(0% 0 0 / 0.04);
--shadow-overlay:
0 8px 30px oklch(0% 0 0 / 0.12), 0 2px 8px oklch(0% 0 0 / 0.06),
0 0 0 1px oklch(0% 0 0 / 0.06);
/* Easing curves */
--ease-smooth: cubic-bezier(0.4, 0, 0.2, 1);
--ease-out-fluid: cubic-bezier(0.32, 0.72, 0, 1);
/* ... */
}
```
**Light mode tokens** (`:root`) — base OKLCH values for all colors, surfaces, and borders.
**Dark mode tokens** (`.dark`) — overrides for dark mode, including zeroed shadows where tonal contrast provides depth.
**CSS utilities** — shared focus, disabled, and invalid patterns used by every component:
```css
@utility focus-ring {
outline: 2px solid var(--color-ring);
outline-offset: 2px;
}
@utility focus-field-ring {
outline: 2px solid var(--color-ring);
outline-offset: -1px;
}
@utility status-disabled {
opacity: 0.5;
pointer-events: none;
cursor: not-allowed;
}
```
For the complete `globals.css` file, see the [Theming](/docs/theming) guide.
### 4. Copy Component Files
Copy the component source from the registry into your `components/ui/` directory. Each component is a self-contained file that imports from `@base-ui/react` and `@/lib/utils`.
```tsx
// components/ui/button.tsx
"use client";
import { Button as ButtonPrimitive } from "@base-ui/react/button";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
// ... component implementation
```
## Token Overview
The design system organizes tokens into clear categories:
| Category | Examples | Purpose |
| ------------ | -------------------------------------------------------- | ---------------------------------------- |
| **Colors** | `--primary`, `--destructive`, `--success` | Brand and semantic colors (OKLCH values) |
| **Surfaces** | `--surface`, `--overlay`, `--field-background` | Role-based surface backgrounds |
| **Borders** | `--border`, `--border-light`, `--field-border` | Edge treatments at multiple weights |
| **Shadows** | `--shadow-surface`, `--shadow-field`, `--shadow-overlay` | Three semantic tiers, mode-adaptive |
| **Easings** | `--ease-smooth`, `--ease-out-fluid` | Animation timing functions |
| **Radius** | `--radius`, `--radius-sm`, `--radius-lg` | Border radius scale |
| **Derived** | `--primary-hover`, `--primary-soft` | Auto-computed via `color-mix()` |
## Dark Mode
Prototyper UI supports dark mode through the `.dark` class on the `` element. The recommended approach uses [next-themes](https://github.com/paisan-s/next-themes):
### Using next-themes
```bash
npm install next-themes
```
```tsx
// app/layout.tsx
import { ThemeProvider } from "next-themes";
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
{children}
);
}
```
### How Dark Mode Works
The `.dark` class toggles all color tokens to their dark variants and adjusts the shadow system:
- **Colors** shift to darker backgrounds and lighter foregrounds
- **Shadows** are zeroed for `surface` and `field` (tonal contrast provides depth instead)
- **Overlay shadows** switch to a subtle white inset glow + deeper dark shadow
- **Derived colors** (`--primary-hover`, `--primary-soft`, etc.) auto-adapt via `color-mix()`
### Alternative: data-theme Attribute
You can also toggle dark mode via a `data-theme` attribute if you prefer:
```css
[data-theme="dark"] {
/* Same dark mode token overrides as .dark */
}
```
### System Preference
To respect the user's OS setting without JavaScript:
```css
@media (prefers-color-scheme: dark) {
:root {
/* Dark mode token overrides */
}
}
```
The `next-themes` approach with `enableSystem` handles this automatically and avoids flash of wrong theme on load.
# Introduction
> A composable UI library built on Base UI with Tailwind CSS. Own every line of code.
URL: https://prototyper-ui.com/docs/introduction
## What is Prototyper UI?
Prototyper UI is a collection of beautifully designed, accessible components built on [Base UI](https://base-ui.com) and styled with [Tailwind CSS v4](https://tailwindcss.com). Components are copied into your project as source files — you own and control every line of code.
Install any component with a single command:
```bash
npx shadcn@latest add https://prototyper-ui.com/r/button.json
```
The component lands in your codebase as a real file you can read, edit, and extend. No runtime dependency, no version lock-in, no abstraction layer to fight.
## Design Principles
### Composable — Own the Code
Components follow the [shadcn model](https://ui.shadcn.com/docs): the library is a starting point, not a dependency. You install a component, it becomes your file. Need to change how a select renders its trigger? Open the file and change it.
### Beautiful by Default
The design system uses OKLCH color tokens, multi-layer shadows, role-based surfaces, and fluid easing curves. Components look exceptional without any customization. Beauty comes from precise tokens and consistent spacing — not decoration.
### Base UI Native
Every interactive component is built on [Base UI](https://base-ui.com), the unstyled primitive library from the Material UI team. Base UI handles focus management, keyboard navigation, ARIA attributes, scroll locking, and portal rendering. Prototyper UI handles design.
### LLM-Friendly
Every component follows identical patterns: `"use client"`, imports, component functions, exports. Consistent naming (`Select`, `SelectTrigger`, `SelectContent`), consistent props (`className` + `...props` spread), consistent styling (`cn()` + Tailwind + `data-slot`). When an LLM reads one component, it understands all of them.
## Comparison with Alternatives
| Feature | Prototyper UI | shadcn/ui | HeroUI | Radix Themes |
| --------------------- | ---------------------------------- | --------------------- | ----------------------- | ----------------------- |
| **Primitive library** | Base UI | Radix UI | React Aria | Radix UI |
| **Styling** | Tailwind-in-component | Tailwind-in-component | Slots + Tailwind | CSS-in-JS |
| **Owns code** | Yes (copy-paste) | Yes (copy-paste) | No (npm) | No (npm) |
| **Color space** | OKLCH | HSL | HSL | Custom |
| **Surface system** | Role-based (surface/overlay/field) | Ad-hoc | Role-based | Ad-hoc |
| **Shadow system** | Semantic (surface/field/overlay) | Size-based (sm/md/lg) | Semantic | Size-based |
| **Component API** | Flat named exports | Flat named exports | Compound (dot notation) | Compound (dot notation) |
| **Dark mode shadows** | Zeroed / inset glow | Same as light | Zeroed / inset glow | Same as light |
## Architecture
Prototyper UI uses a **registry-based architecture**, compatible with the shadcn CLI:
```
Registry (prototyper-ui.com/r/*.json)
→ Component source files (registry/ui/*.tsx)
→ Your project (components/ui/*.tsx)
```
1. The **registry** hosts JSON manifests that describe each component, its source code, and its dependencies.
2. The **CLI** (`npx shadcn@latest add`) reads the manifest, resolves dependencies, and copies the component source into your project.
3. **Your project** owns the resulting files. Components import from `@/components/ui/*` and `@/lib/utils` — standard paths that work in any Next.js project.
### Key Dependencies
Every component builds on the same foundation:
- [**Base UI**](https://base-ui.com) (`@base-ui/react`) — Unstyled, accessible primitives
- [**Tailwind CSS v4**](https://tailwindcss.com) (`tailwindcss`) — Utility-first CSS framework
- [**class-variance-authority**](https://cva.style) (`class-variance-authority`) — Variant management for components
- [**tailwind-merge**](https://github.com/dcastil/tailwind-merge) (`tailwind-merge`) — Intelligent class merging
- [**clsx**](https://github.com/lukeed/clsx) (`clsx`) — Conditional class construction
### Utility Function
All components use a shared `cn()` utility that combines `clsx` and `tailwind-merge`:
```ts
// lib/utils.ts
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
```
This lets you safely merge and override Tailwind classes when composing components.
# Machine Mode Package
> Add an LLM-friendly plain text view to any Next.js App Router site.
URL: https://prototyper-ui.com/docs/machine-mode
## Overview
`@prototyperco/machine-mode` is an installable runtime that adds a "machine mode" to any Next.js App Router site. It gives every page a URL-driven plain text view that LLMs can read, with a full-page shell UI for humans to preview what machines see.
Features:
- URL-driven mode switching (`?view=machine`)
- Rendered/raw format toggle (`?format=rendered|raw`)
- Full-page machine shell UI with utility bar and copy action
- Server route helpers for `/machine/[...slug]` and `/machine.txt`
- Keyboard shortcuts (`Shift+M` to toggle, `R` to switch format)
- Client-side text caching and markdown rendering
## Requirements
- Next.js 15 or 16 (App Router)
- React 19+
- Tailwind CSS v4
## Entry Points
The package provides four entry points:
| Entry point | Environment | Description |
| --------------------------------------- | ----------- | --------------------------------------- |
| `@prototyperco/machine-mode` | Both | Barrel re-export of client + server |
| `@prototyperco/machine-mode/client` | Client | Provider, gate, shell, toggle, hooks |
| `@prototyperco/machine-mode/server` | Server | Route handlers, resolver types |
| `@prototyperco/machine-mode/styles.css` | CSS | Machine mode theme tokens and utilities |
## Quick Start (CLI)
```bash
bunx @prototyperco/cli machine-mode init
```
This scaffolds:
- `lib/machine-resolver.ts`
- `app/machine/[...slug]/route.ts`
- `app/machine.txt/route.ts`
- `app/layout.tsx` wiring with `MachineModeProvider` + `MachineGate`
- `globals.css` import for `@prototyperco/machine-mode/styles.css`
## Manual Setup
### Install package
```bash
pnpm add @prototyperco/machine-mode
```
### Import styles
```css title="app/globals.css"
@import "@prototyperco/machine-mode/styles.css";
```
### Wire layout
```tsx title="app/layout.tsx"
import { Suspense } from "react";
import {
MachineGate,
MachineModeProvider,
} from "@prototyperco/machine-mode/client";
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
{children}
);
}
```
`MachineModeProvider` uses `useSearchParams()` internally and must be wrapped in a `Suspense` boundary. It reads `?view=machine` from the URL and provides mode state to the tree. `MachineGate` conditionally renders either your normal page or the `MachineShell` overlay.
### Create a resolver
The resolver maps pathnames to machine-readable content. It can be sync or async.
```ts title="lib/machine-resolver.ts"
import type { MachineDoc } from "@prototyperco/machine-mode/server";
export async function resolveMachineDoc(
pathname: string,
): Promise {
if (pathname === "/") {
return {
pathname,
title: "Home",
sourceUrl: pathname,
content: "# Home\n\nWelcome to this site in machine mode.",
contentType: "curated",
generatedAt: new Date().toISOString(),
};
}
return null;
}
```
### Add route handlers
```ts title="app/machine/[...slug]/route.ts"
import { createMachineRouteHandler } from "@prototyperco/machine-mode/server";
import { resolveMachineDoc } from "@/lib/machine-resolver";
export const GET = createMachineRouteHandler({ resolveDoc: resolveMachineDoc });
```
```ts title="app/machine.txt/route.ts"
import { createMachineIndexHandler } from "@prototyperco/machine-mode/server";
export const GET = createMachineIndexHandler();
```
## Resolver Contract
Your resolver receives a normalized pathname and returns either:
- A `MachineDoc` object for known routes
- `null` to use the built-in fallback content
The resolver can be synchronous or asynchronous:
```ts
type MachineDocResolver = (
pathname: string,
) => MachineDoc | null | undefined | Promise;
```
```ts
interface MachineDoc {
pathname: string;
title: string;
sourceUrl: string;
content: string; // markdown string
contentType: "docs" | "curated" | "fallback" | string;
generatedAt: string; // ISO 8601
}
```
When the resolver returns `null`, the route handler uses `createFallbackMachineDoc()` to generate a placeholder response that links back to the human page, the machine index, and the LLM index.
## API Reference
### Client Exports
Imported from `@prototyperco/machine-mode/client`.
#### MachineModeProvider
Reads `?view=machine` and `?format=rendered|raw` from the URL and provides mode context to the component tree. Registers the `Shift+M` keyboard shortcut.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `children` | `React.ReactNode` | - | Application content. |
| `viewQueryParam` | `string` | `"view"` | URL query parameter that controls mode. |
| `formatQueryParam` | `string` | `"format"` | URL query parameter that controls render format. |
| `machineViewValue` | `string` | `"machine"` | Value of the view param that activates machine mode. |
| `defaultRenderMode` | `"rendered" \| "raw"` | `"rendered"` | Default format when entering machine mode. |
| `machineEndpointBasePath` | `string` | `"/machine"` | Base path for the machine content API routes. |
#### MachineGate
Renders either `children` (human mode) or `MachineShell` (machine mode), with enter/exit transitions.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `children` | `React.ReactNode` | - | Normal page content shown in human mode. |
#### MachineShell
Full-page machine view overlay with utility bar, rendered/raw viewport, copy button, and metadata footer. Fetches content from the `/machine/[...slug]` route.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `state` | `"visible" \| "hiding"` | - | Controls enter/exit animation phase. |
#### ModeToggle
Floating or inline Human/Machine segmented control.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `variant` | `"floating" \| "inline"` | `"floating"` | Positioning style. Floating is fixed at viewport bottom-center. |
| `className` | `string` | - | Additional CSS classes. |
#### useMachineMode
Hook that returns the current mode context:
```ts
function useMachineMode(): {
mode: "human" | "machine";
setMode: (mode: "human" | "machine") => void;
ready: boolean;
config: MachineModeConfig;
};
```
`useMode` is an alias for `useMachineMode`.
`MachineModeConfig` contains the resolved provider options:
```ts
interface MachineModeConfig {
viewQueryParam: string;
formatQueryParam: string;
machineViewValue: string;
defaultRenderMode: "rendered" | "raw";
machineEndpointBasePath: string;
}
```
#### renderMachineMarkdown
Parses a markdown string into a styled React node for the machine shell viewport.
```ts
function renderMachineMarkdown(markdown: string): {
node: React.ReactNode;
error: string | null;
};
```
#### Utility Functions
```ts
// Client-side text cache
function getCachedText(key: string): string | undefined;
function setCachedText(key: string, value: string): void;
function fetchCachedText(
url: string,
options?: { signal?: AbortSignal },
): Promise;
// View/render mode parsing
function parseMachineViewMode(
value: string | null,
machineViewValue?: string,
): "human" | "machine" | null;
function parseMachineRenderMode(
value: string | null,
): "rendered" | "raw" | null;
function isEditableElementTarget(target: EventTarget | null): boolean;
```
#### Pathname Helpers
Shared between client and server:
```ts
function normalizeWebsitePathname(pathname: string): string;
function normalizeMachineBasePath(basePath: string): string;
function toMachineEndpointPath(
pathname: string,
machineBasePath?: string,
): string;
function machineSegmentsToWebsitePathname(segments: string[]): string;
```
#### Exported Types
The client entry also exports these TypeScript types:
- `MachineViewMode` -- `"human" | "machine"`
- `MachineRenderMode` -- `"rendered" | "raw"`
- `MachineModeConfig` -- Resolved provider configuration (see `useMachineMode` above)
- `MachineModeProviderProps` -- Props for `MachineModeProvider`
- `MachineMarkdownRenderResult` -- Return type of `renderMachineMarkdown`
### Server Exports
Imported from `@prototyperco/machine-mode/server`.
#### createMachineRouteHandler
Creates a Next.js route handler for `app/machine/[...slug]/route.ts`. Calls your resolver with the normalized pathname and returns the content as `text/plain`.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `resolveDoc` | `MachineDocResolver` | - | Function that maps a pathname to a MachineDoc or null. |
| `cacheControl` | `string` | `"public, s-maxage=3600, stale-while-revalidate=86400"` | Cache-Control header value for responses. |
| `siteUrl` | `string` | - | Absolute site URL used in fallback content links. |
| `machineIndexPath` | `string` | `"/machine.txt"` | Path to the machine index, used in fallback content. |
| `llmsIndexPath` | `string` | `"/llms.txt"` | Path to the LLMs index, used in fallback content. |
| `fallbackResolver` | `(pathname: string) => MachineDoc` | - | Custom fallback when the resolver returns null. Overrides the default fallback. |
Response headers include `X-Machine-Title`, `X-Machine-Source-Url`, `X-Machine-Content-Type`, and `X-Machine-Generated-At`.
#### createMachineIndexHandler
Creates a Next.js route handler for `app/machine.txt/route.ts`. Returns a plain text index of machine-mode URLs and endpoints.
| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `cacheControl` | `string` | `"public, s-maxage=3600, stale-while-revalidate=86400"` | Cache-Control header value. |
| `siteUrl` | `string` | - | Absolute site URL prepended to paths in the index. |
| `machineBasePath` | `string` | `"/machine"` | Base path for raw machine endpoints. |
| `viewQueryParam` | `string` | `"view"` | View query parameter name shown in the URL contract. |
| `formatQueryParam` | `string` | `"format"` | Format query parameter name shown in the URL contract. |
| `getKnownRoutes` | `() => string[] \| Promise` | - | Returns known routes to list in the "Route Coverage" section. |
| `includeLegacyIndexes` | `string[]` | `["/llms.txt"]` | Legacy LLM endpoint paths to include in the index. |
| `buildIndexText` | `() => string \| Promise` | - | Fully custom index builder. Overrides all other options. |
#### buildMachineIndexText
Generates the machine index text content without wrapping it in a route handler. Accepts the same options as `createMachineIndexHandler` (minus `cacheControl` and `buildIndexText`).
#### createFallbackMachineDoc
Creates a fallback `MachineDoc` for routes the resolver does not handle:
```ts
function createFallbackMachineDoc(
pathname: string,
options?: {
siteUrl?: string;
machineIndexPath?: string;
llmsIndexPath?: string;
},
): MachineDoc;
```
#### Exported Types
The server entry also exports these TypeScript types:
- `MachineDoc` -- Machine document shape returned by resolvers
- `MachineContentType` -- `"docs" | "curated" | "fallback" | (string & {})`
- `MachineDocResolver` -- Resolver function signature
- `MachineRouteHandlerOptions` -- Options for `createMachineRouteHandler`
- `MachineIndexHandlerOptions` -- Options for `createMachineIndexHandler`
## Keyboard Interactions
| Key | Context | Action |
| --------- | -------------------------- | -------------------------------------- |
| `Shift+M` | Any non-editable element | Toggle between human and machine mode |
| `R` | Machine mode, non-editable | Switch between rendered and raw format |
Both shortcuts are disabled when focus is on an ` `, ``, ``, or `contentEditable` element.
## Styling
The styles from `@prototyperco/machine-mode/styles.css` define machine-specific design tokens using OKLCH color values. The same tokens apply in both light and dark mode (machine mode is always dark).
| Token | Default | Purpose |
| ---------------------------- | ----------------------------- | ---------------------------------- |
| `--machine-bg` | `2.8% 0 0` | Shell background |
| `--machine-panel` | `4.4% 0 0` | Panel/header background |
| `--machine-panel-raised` | `7.2% 0 0` | Raised panel background |
| `--machine-fg` | `98% 0 0` | Primary text |
| `--machine-fg-muted` | `88% 0 0` | Body text |
| `--machine-fg-subtle` | `72% 0 0` | De-emphasized text |
| `--machine-border` | `100% 0 0 / 0.22` | Border color |
| `--machine-rule` | `100% 0 0 / 0.3` | Separator/rule color |
| `--machine-inverse` | `96% 0 0` | Inverse background (active toggle) |
| `--machine-inverse-fg` | `8% 0 0` | Inverse foreground text |
| `--machine-font-size-base` | `0.95rem` | Base font size |
| `--machine-line-height-base` | `1.7` | Base line height |
| `--machine-heading-weight` | `640` | Heading font weight |
| `--machine-code-size` | `0.84rem` | Code block font size |
| `--machine-content-max` | `76ch` | Max content width |
| `--machine-section-gap` | `2.5rem` | Gap before h2 headings |
| `--machine-block-gap` | `1rem` | Gap between block elements |
| `--machine-rail-offset` | `clamp(0.9rem, 2vw, 1.45rem)` | Horizontal content padding |
Override any token in your own CSS to customize the machine mode appearance.
## Migration From Local Implementation
1. Install `@prototyperco/machine-mode`.
2. Move app-specific content resolution into `lib/machine-resolver.ts`.
3. Replace local mode provider/gate/shell imports with package imports from `@prototyperco/machine-mode/client`.
4. Replace local route handler logic with `createMachineRouteHandler` and `createMachineIndexHandler` from `@prototyperco/machine-mode/server`.
5. Replace local machine CSS with `@import "@prototyperco/machine-mode/styles.css"`.
6. Keep existing `/llms*` endpoints untouched -- they are separate from machine mode.
## Notes
- App Router only (Pages Router is not supported).
- Machine mode is explicit via URL query params -- no bot/user-agent auto-detection.
- The package is additive; your existing component workflow stays the same.
- The barrel entry point (`@prototyperco/machine-mode`) re-exports everything from both `/client` and `/server`. Use the sub-path imports when you need tree-shaking or want to be explicit about the environment.
# Theming
> Customize colors, surfaces, and shadows using OKLCH tokens and CSS custom properties.
URL: https://prototyper-ui.com/docs/theming
Prototyper UI's design system is built on CSS custom properties using the OKLCH color space. Every color, surface, shadow, and easing curve is a token you can override.
## OKLCH Color System
All colors use the [OKLCH color space](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/oklch) — a perceptually uniform model where lightness, chroma, and hue are independent axes.
```css
/* OKLCH values: lightness chroma hue */
--primary: 39.11% 0.084 240.8;
```
### Why OKLCH?
- **Perceptually uniform** — a 10% lightness shift looks like 10% regardless of the base hue. HSL lightness is wildly inconsistent across colors.
- **Predictable color-mix()** — derived states (hover, soft) use `color-mix(in oklab)` and produce consistent results across the entire hue range.
- **Wide gamut** — OKLCH can represent P3 and Rec.2020 colors that HSL cannot.
### How Derived Colors Work
Hover states, soft variants, and surface tiers are computed automatically using `color-mix()`:
```css
/* Hover: 90% base + 10% paired foreground */
--primary-hover: color-mix(
in oklab,
oklch(var(--primary)) 90%,
oklch(var(--primary-foreground)) 10%
);
/* Soft variant: 15% base on transparent */
--primary-soft: color-mix(in oklab, oklch(var(--primary)) 15%, transparent);
/* Surface tiers: progressive foreground mixing */
--surface-secondary: color-mix(
in oklab,
oklch(var(--surface)) 94%,
oklch(var(--surface-foreground)) 6%
);
--surface-tertiary: color-mix(
in oklab,
oklch(var(--surface)) 88%,
oklch(var(--surface-foreground)) 12%
);
```
You only need to define the base color and its foreground — all interaction states derive automatically.
## Token Reference
### Base Colors
| Token | Description | Light Default |
| -------------- | ------------------ | -------------------- |
| `--background` | Page background | `100% 0 0` (white) |
| `--foreground` | Default text color | `14.05% 0.004 285.8` |
| `--radius` | Base border radius | `0.5rem` |
### Primary Ramp
| Token | Description | Light Default |
| ---------------------- | ---------------------------- | -------------------- |
| `--primary` | Primary brand color, buttons | `39.11% 0.084 240.8` |
| `--primary-foreground` | Text on primary backgrounds | `98.48% 0 0` |
| `--primary-light` | Gradient light stop | `75.84% 0.137 231.6` |
| `--primary-middle` | Gradient middle stop | `49.96% 0.118 242.2` |
| `--primary-dark` | Gradient dark stop | `39.20% 0.084 240.8` |
### Semantic Colors
| Token | Description |
| -------------------------------------------- | ------------------------------------ |
| `--secondary` / `--secondary-foreground` | Secondary actions, subtle buttons |
| `--muted` / `--muted-foreground` | Muted backgrounds, placeholder text |
| `--accent` / `--accent-foreground` | Accent highlights, hover backgrounds |
| `--destructive` / `--destructive-foreground` | Destructive actions, error states |
| `--success` / `--success-foreground` | Success states |
| `--warning` / `--warning-foreground` | Warning states |
| `--info` / `--info-foreground` | Informational states |
### Surfaces
| Token | Description |
| ------------------------------------ | ------------------------------------------- |
| `--surface` / `--surface-foreground` | Cards, panels, tabs |
| `--surface-secondary` | Nested containers (derived via `color-mix`) |
| `--surface-tertiary` | Deeper nesting (derived via `color-mix`) |
| `--overlay` / `--overlay-foreground` | Dialogs, popovers, menus |
| `--card` / `--card-foreground` | Card backgrounds |
| `--popover` / `--popover-foreground` | Popover backgrounds |
### Fields
| Token | Description |
| ------------------------ | ------------------------------------------ |
| `--field-background` | Input, select, combobox backgrounds |
| `--field-border` | Field border color |
| `--field-border-hover` | Field border on hover (derived) |
| `--field-border-invalid` | Field border on validation error (derived) |
### Borders
| Token | Description |
| ---------------- | -------------------------------------- |
| `--border` | Default border |
| `--border-light` | Subtle border (dividers, separators) |
| `--border-dark` | Emphasized border |
| `--input` | Input border (matches `--border`) |
| `--ring` | Focus ring color (matches `--primary`) |
### Shadows
| Token | Light Mode | Dark Mode |
| ------------------ | ------------------------- | ---------------------------------- |
| `--shadow-surface` | Multi-layer subtle shadow | `none` |
| `--shadow-field` | Subtle shadow + 1px edge | `none` |
| `--shadow-overlay` | Heavy multi-layer shadow | 1px white inset glow + deep shadow |
### Easings
| Token | Value | Usage |
| -------------------- | ----------------------------------------- | ---------------------------- |
| `--ease-smooth` | `cubic-bezier(0.4, 0, 0.2, 1)` | General transitions |
| `--ease-out-fluid` | `cubic-bezier(0.32, 0.72, 0, 1)` | Signature deceleration curve |
| `--ease-out-quad` | `cubic-bezier(0.25, 0.46, 0.45, 0.94)` | Subtle ease-out |
| `--ease-out-quart` | `cubic-bezier(0.165, 0.84, 0.44, 1)` | Pronounced ease-out |
| `--ease-in-quad` | `cubic-bezier(0.55, 0.085, 0.68, 0.53)` | Subtle ease-in |
| `--ease-in-quart` | `cubic-bezier(0.895, 0.03, 0.685, 0.22)` | Pronounced ease-in |
| `--ease-in-out-quad` | `cubic-bezier(0.455, 0.03, 0.515, 0.955)` | Symmetric ease |
### Derived Interaction Colors
These are auto-computed from base tokens — you typically do not override them:
| Token | Formula |
| -------------------------- | ---------------------------------------------------- |
| `--primary-hover` | 90% `--primary` + 10% `--primary-foreground` |
| `--destructive-hover` | 90% `--destructive` + 10% `--destructive-foreground` |
| `--success-hover` | 90% `--success` + 10% `--success-foreground` |
| `--warning-hover` | 90% `--warning` + 10% `--warning-foreground` |
| `--accent-hover` | 92% `--accent` + 8% `--accent-foreground` |
| `--primary-soft` | 15% `--primary` on transparent |
| `--primary-soft-hover` | 20% `--primary` on transparent |
| `--destructive-soft` | 15% `--destructive` on transparent |
| `--destructive-soft-hover` | 20% `--destructive` on transparent |
## Customize Colors
Override any token in your `globals.css` to change the look of all components at once. Tokens use raw OKLCH values (lightness, chroma, hue) without the `oklch()` wrapper:
```css
:root {
/* Change primary to a teal */
--primary: 55% 0.15 180;
--primary-foreground: 98% 0 0;
--primary-light: 75% 0.12 175;
--primary-middle: 60% 0.14 178;
--primary-dark: 45% 0.13 182;
/* Update the focus ring to match */
--ring: 55% 0.15 180;
}
.dark {
--primary: 40% 0.1 180;
--primary-foreground: 95% 0.02 178;
--primary-light: 75% 0.12 175;
--primary-middle: 60% 0.14 178;
--primary-dark: 45% 0.13 182;
--ring: 70% 0.08 178;
}
```
All derived tokens (`--primary-hover`, `--primary-soft`, etc.) will auto-adapt because they use `color-mix()` with your new base values.
## Create a Custom Theme
Here is a full example of a custom theme with both light and dark modes. Copy this into your `globals.css` and adjust the values:
```css
:root {
/* Base */
--background: 100% 0 0;
--foreground: 14.05% 0.004 285.8;
--radius: 0.5rem;
/* Primary — violet example */
--primary: 50% 0.2 280;
--primary-foreground: 98% 0 0;
--primary-light: 75% 0.15 275;
--primary-middle: 58% 0.18 278;
--primary-dark: 42% 0.19 282;
/* Secondary */
--secondary: 96% 0.01 280;
--secondary-foreground: 25% 0.03 280;
/* Muted */
--muted: 96% 0 0;
--muted-foreground: 55% 0.01 285;
/* Accent */
--accent: 96% 0 0;
--accent-foreground: 21% 0.006 285;
/* Destructive */
--destructive: 63% 0.21 25;
--destructive-foreground: 98% 0 0;
/* Borders */
--border: 92% 0.004 286;
--border-light: 96% 0 0;
--border-dark: 80% 0.01 286;
--input: 92% 0.004 286;
--ring: 50% 0.2 280;
/* Surfaces */
--surface: 98% 0 0;
--surface-foreground: 14% 0.004 285;
--surface-secondary: color-mix(
in oklab,
oklch(var(--surface)) 94%,
oklch(var(--surface-foreground)) 6%
);
--surface-tertiary: color-mix(
in oklab,
oklch(var(--surface)) 88%,
oklch(var(--surface-foreground)) 12%
);
--overlay: 100% 0 0;
--overlay-foreground: 14% 0.004 285;
/* Cards & Popovers */
--card: 98% 0 0;
--card-foreground: 14% 0.004 285;
--popover: 100% 0 0;
--popover-foreground: 14% 0.004 285;
/* Fields */
--field-background: 100% 0 0;
--field-border: 92% 0.004 286;
--field-border-hover: color-mix(
in oklab,
oklch(var(--field-border)) 70%,
oklch(var(--foreground)) 30%
);
--field-border-invalid: oklch(var(--destructive));
/* Derived hover states — auto-adapt */
--primary-hover: color-mix(
in oklab,
oklch(var(--primary)) 90%,
oklch(var(--primary-foreground)) 10%
);
--destructive-hover: color-mix(
in oklab,
oklch(var(--destructive)) 90%,
oklch(var(--destructive-foreground)) 10%
);
--accent-hover: color-mix(
in oklab,
oklch(var(--accent)) 92%,
oklch(var(--accent-foreground)) 8%
);
/* Derived soft variants */
--primary-soft: color-mix(in oklab, oklch(var(--primary)) 15%, transparent);
--primary-soft-hover: color-mix(
in oklab,
oklch(var(--primary)) 20%,
transparent
);
--destructive-soft: color-mix(
in oklab,
oklch(var(--destructive)) 15%,
transparent
);
--destructive-soft-hover: color-mix(
in oklab,
oklch(var(--destructive)) 20%,
transparent
);
}
.dark {
/* Base */
--background: 14% 0.004 285;
--foreground: 98% 0 0;
/* Primary — violet dark mode */
--primary: 35% 0.14 282;
--primary-foreground: 95% 0.02 278;
--primary-light: 75% 0.15 275;
--primary-middle: 58% 0.18 278;
--primary-dark: 42% 0.19 282;
/* Secondary */
--secondary: 28% 0.03 280;
--secondary-foreground: 98% 0 0;
/* Muted */
--muted: 27% 0.006 286;
--muted-foreground: 71% 0.013 286;
/* Accent */
--accent: 27% 0.006 286;
--accent-foreground: 98% 0 0;
/* Destructive */
--destructive: 40% 0.13 26;
--destructive-foreground: 98% 0 0;
/* Borders */
--border: 32% 0.007 286;
--border-light: 37% 0.008 286;
--border-dark: 23% 0.004 286;
--input: 27% 0.006 286;
--ring: 70% 0.1 278;
/* Surfaces */
--surface: 16% 0.006 285;
--surface-foreground: 98% 0 0;
--surface-secondary: color-mix(
in oklab,
oklch(var(--surface)) 94%,
oklch(var(--surface-foreground)) 6%
);
--surface-tertiary: color-mix(
in oklab,
oklch(var(--surface)) 88%,
oklch(var(--surface-foreground)) 12%
);
--overlay: 20% 0.008 285;
--overlay-foreground: 98% 0 0;
/* Cards & Popovers */
--card: 16% 0.006 285;
--card-foreground: 98% 0 0;
--popover: 14% 0.004 285;
--popover-foreground: 98% 0 0;
/* Fields */
--field-background: 16% 0.006 285;
--field-border: 32% 0.007 286;
--field-border-hover: color-mix(
in oklab,
oklch(var(--field-border)) 70%,
oklch(var(--foreground)) 30%
);
--field-border-invalid: oklch(var(--destructive));
/* Derived — same formulas, auto-adapt to dark values */
--primary-hover: color-mix(
in oklab,
oklch(var(--primary)) 90%,
oklch(var(--primary-foreground)) 10%
);
--destructive-hover: color-mix(
in oklab,
oklch(var(--destructive)) 90%,
oklch(var(--destructive-foreground)) 10%
);
--accent-hover: color-mix(
in oklab,
oklch(var(--accent)) 92%,
oklch(var(--accent-foreground)) 8%
);
--primary-soft: color-mix(in oklab, oklch(var(--primary)) 15%, transparent);
--primary-soft-hover: color-mix(
in oklab,
oklch(var(--primary)) 20%,
transparent
);
--destructive-soft: color-mix(
in oklab,
oklch(var(--destructive)) 15%,
transparent
);
--destructive-soft-hover: color-mix(
in oklab,
oklch(var(--destructive)) 20%,
transparent
);
/* Shadows — tonal contrast replaces shadows */
--shadow-surface: none;
--shadow-field: none;
--shadow-overlay:
0 0 0 1px oklch(100% 0 0 / 0.08), 0 8px 30px oklch(0% 0 0 / 0.35);
}
```
### What to Define vs. What to Derive
| Define manually | Derived via `color-mix()` |
| ------------------------------------------------ | ---------------------------------------- |
| Base colors (`--primary`, `--destructive`, etc.) | Hover states (90% base + 10% foreground) |
| Foreground colors (`--primary-foreground`, etc.) | Soft variants (15% base + transparent) |
| Color ramps (`--primary-light/middle/dark`) | Surface tiers (secondary, tertiary) |
| Surface/overlay/field backgrounds | Border hover progressions |
Foreground colors cannot be auto-derived — green needs dark text in both modes, while blue needs light text in both modes. Ramp derivation (light/middle/dark stops) collapses for light colors and washes chroma for dark colors. Define where it matters, derive where it's safe.
## Runtime Theme Switching
### Toggle Dark Mode
Add or remove the `.dark` class on the `` element:
```ts
// Toggle dark mode
document.documentElement.classList.toggle("dark");
// Set explicitly
document.documentElement.classList.add("dark"); // dark
document.documentElement.classList.remove("dark"); // light
```
With [next-themes](https://github.com/paisan-s/next-themes), use the `useTheme` hook:
```tsx
import { useTheme } from "next-themes";
function ThemeToggle() {
const { theme, setTheme } = useTheme();
return (
setTheme(theme === "dark" ? "light" : "dark")}>
Toggle theme
);
}
```
### data-theme Attribute
You can also switch themes using a `data-theme` attribute, which is useful when supporting multiple named themes beyond light/dark:
```html
```
```css
[data-theme="dark"] {
--background: 14.05% 0.004 285.8;
--foreground: 98.48% 0 0;
/* ... all dark mode overrides */
}
```
### Swap Color Themes at Runtime
To switch between color themes (e.g., different primary colors) at runtime, apply CSS custom property overrides via a class or attribute:
```css
[data-theme="violet"] {
--primary: 50% 0.2 280;
--primary-foreground: 98% 0 0;
--primary-light: 75% 0.15 275;
--primary-middle: 58% 0.18 278;
--primary-dark: 42% 0.19 282;
--ring: 50% 0.2 280;
}
[data-theme="violet"].dark {
--primary: 35% 0.14 282;
--primary-foreground: 95% 0.02 278;
--ring: 70% 0.1 278;
}
```
```ts
// Switch color theme
document.documentElement.setAttribute("data-theme", "violet");
```
## Theme Builder
Use the interactive [Theme Builder](/design) to preview color combinations and generate the CSS tokens for your custom theme.
# Design Tokens
> Complete reference of all Prototyper UI design tokens — colors, surfaces, shadows, radius, typography, and animation.
URL: https://prototyper-ui.com/docs/tokens
{/* Auto-generated by scripts/generate-token-docs.ts — do not edit manually */}
Prototyper UI uses OKLCH color tokens with `color-mix()` for derived states. All tokens are defined in `prototyper-tokens.css` and registered with Tailwind CSS v4's `@theme` directive.
## Token Overview
| Category | Count |
| ------------------------- | ----- |
| [Colors](#colors) | 102 |
| [Surfaces](#surfaces) | 34 |
| [Shadows](#shadows) | 5 |
| [Radius](#radius) | 7 |
| [Typography](#typography) | 18 |
| [Spacing](#spacing) | 1 |
| [Animation](#animation) | 10 |
## Colors
Core color tokens in OKLCH format. These define the color palette for all components.
| Token | Light | Dark |
| -------------------------------- | ---------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- |
| `--accent` | `96.76% 0 0` | `27.41% 0.006 286.0` |
| `--accent-foreground` | `21.03% 0.006 285.9` | `98.48% 0 0` |
| `--accent-hover` | `color-mix(in oklab, oklch(var(--accent)) 92%, oklch(var(--accent-foreground)) 8%)` | `color-mix(in oklab, oklch(var(--accent)) 92%, oklch(var(--accent-foreground)) 8%)` |
| `--background` | `100% 0 0` | `14.05% 0.004 285.8` |
| `--border` | `91.97% 0.004 286.3` | `31.51% 0.007 286.0` |
| `--border-dark` | `79.59% 0.011 286.2` | `23.37% 0.004 286.1` |
| `--border-light` | `96.01% 0 0` | `37.27% 0.008 286.0` |
| `--color-accent` | `oklch(var(--accent))` | — |
| `--color-accent-foreground` | `oklch(var(--accent-foreground))` | — |
| `--color-accent-hover` | `var(--accent-hover)` | — |
| `--color-background` | `oklch(var(--background))` | — |
| `--color-border` | `oklch(var(--border))` | — |
| `--color-border-dark` | `oklch(var(--border-dark))` | — |
| `--color-border-light` | `oklch(var(--border-light))` | — |
| `--color-card` | `oklch(var(--card))` | — |
| `--color-card-foreground` | `oklch(var(--card-foreground))` | — |
| `--color-destructive` | `oklch(var(--destructive))` | — |
| `--color-destructive-foreground` | `oklch(var(--destructive-foreground))` | — |
| `--color-destructive-hover` | `var(--destructive-hover)` | — |
| `--color-destructive-soft` | `var(--destructive-soft)` | — |
| `--color-destructive-soft-hover` | `var(--destructive-soft-hover)` | — |
| `--color-field-background` | `oklch(var(--field-background))` | — |
| `--color-field-border` | `var(--field-border)` | — |
| `--color-field-border-focus` | `var(--field-border-focus)` | — |
| `--color-field-border-hover` | `var(--field-border-hover)` | — |
| `--color-field-border-invalid` | `var(--field-border-invalid)` | — |
| `--color-foreground` | `oklch(var(--foreground))` | — |
| `--color-info` | `oklch(var(--info))` | — |
| `--color-info-foreground` | `oklch(var(--info-foreground))` | — |
| `--color-input` | `oklch(var(--input))` | — |
| `--color-machine-bg` | `oklch(var(--machine-bg))` | — |
| `--color-machine-border` | `oklch(var(--machine-border))` | — |
| `--color-machine-fg` | `oklch(var(--machine-fg))` | — |
| `--color-machine-fg-muted` | `oklch(var(--machine-fg-muted))` | — |
| `--color-machine-fg-subtle` | `oklch(var(--machine-fg-subtle))` | — |
| `--color-machine-inverse` | `oklch(var(--machine-inverse))` | — |
| `--color-machine-inverse-fg` | `oklch(var(--machine-inverse-fg))` | — |
| `--color-machine-panel` | `oklch(var(--machine-panel))` | — |
| `--color-machine-panel-raised` | `oklch(var(--machine-panel-raised))` | — |
| `--color-machine-rule` | `oklch(var(--machine-rule))` | — |
| `--color-muted` | `oklch(var(--muted))` | — |
| `--color-muted-foreground` | `oklch(var(--muted-foreground))` | — |
| `--color-overlay` | `oklch(var(--overlay))` | — |
| `--color-overlay-backdrop` | `var(--overlay-backdrop)` | — |
| `--color-overlay-foreground` | `oklch(var(--overlay-foreground))` | — |
| `--color-popover` | `oklch(var(--popover))` | — |
| `--color-popover-foreground` | `oklch(var(--popover-foreground))` | — |
| `--color-primary` | `oklch(var(--primary))` | — |
| `--color-primary-dark` | `oklch(var(--primary-dark))` | — |
| `--color-primary-foreground` | `oklch(var(--primary-foreground))` | — |
| `--color-primary-hover` | `var(--primary-hover)` | — |
| `--color-primary-light` | `oklch(var(--primary-light))` | — |
| `--color-primary-middle` | `oklch(var(--primary-middle))` | — |
| `--color-primary-soft` | `var(--primary-soft)` | — |
| `--color-primary-soft-hover` | `var(--primary-soft-hover)` | — |
| `--color-ring` | `oklch(var(--ring))` | — |
| `--color-secondary` | `oklch(var(--secondary))` | — |
| `--color-secondary-foreground` | `oklch(var(--secondary-foreground))` | — |
| `--color-success` | `oklch(var(--success))` | — |
| `--color-success-foreground` | `oklch(var(--success-foreground))` | — |
| `--color-success-hover` | `var(--success-hover)` | — |
| `--color-surface` | `oklch(var(--surface))` | — |
| `--color-surface-foreground` | `oklch(var(--surface-foreground))` | — |
| `--color-surface-secondary` | `var(--surface-secondary)` | — |
| `--color-surface-tertiary` | `var(--surface-tertiary)` | — |
| `--color-syntax-component` | `oklch(74% 0.14 220)` | — |
| `--color-syntax-key` | `oklch(74% 0.14 220)` | — |
| `--color-syntax-keyword` | `oklch(72% 0.17 300)` | — |
| `--color-syntax-number` | `oklch(80% 0.15 85)` | — |
| `--color-syntax-string` | `oklch(72% 0.17 160)` | — |
| `--color-syntax-tag` | `oklch(72% 0.16 15)` | — |
| `--color-warning` | `oklch(var(--warning))` | — |
| `--color-warning-foreground` | `oklch(var(--warning-foreground))` | — |
| `--color-warning-hover` | `var(--warning-hover)` | — |
| `--destructive` | `63.68% 0.208 25.3` | `58% 0.16 25.7` |
| `--destructive-foreground` | `98.43% 0 0` | `98.43% 0 0` |
| `--destructive-hover` | `color-mix(in oklab, oklch(var(--destructive)) 90%, oklch(var(--destructive-foreground)) 10%)` | `color-mix(in oklab, oklch(var(--destructive)) 90%, oklch(var(--destructive-foreground)) 10%)` |
| `--destructive-soft` | `color-mix(in oklab, oklch(var(--destructive)) 15%, transparent)` | `color-mix(in oklab, oklch(var(--destructive)) 20%, transparent)` |
| `--destructive-soft-hover` | `color-mix(in oklab, oklch(var(--destructive)) 20%, transparent)` | `color-mix(in oklab, oklch(var(--destructive)) 28%, transparent)` |
| `--foreground` | `14.05% 0.004 285.8` | `98.48% 0 0` |
| `--info` | `62.61% 0.186 259.6` | `62.61% 0.186 259.6` |
| `--info-foreground` | `100% 0 0` | `38.14% 0.136 265.3` |
| `--input` | `91.97% 0.004 286.3` | `27.41% 0.006 286.0` |
| `--muted` | `96.76% 0 0` | `27.41% 0.006 286.0` |
| `--muted-foreground` | `51% 0.014 285.9` | `71.19% 0.013 286.1` |
| `--primary` | `44% 0.017 286` | `55% 0.016 286` |
| `--primary-dark` | `37% 0.016 286` | `44% 0.017 286` |
| `--primary-foreground` | `100% 0 0` | `100% 0 0` |
| `--primary-hover` | `color-mix(in oklab, oklch(var(--primary)) 90%, oklch(var(--primary-foreground)) 10%)` | `color-mix(in oklab, oklch(var(--primary)) 90%, oklch(var(--primary-foreground)) 10%)` |
| `--primary-light` | `87% 0.006 286` | `87% 0.006 286` |
| `--primary-middle` | `55% 0.016 286` | `71% 0.015 286` |
| `--primary-soft` | `color-mix(in oklab, oklch(var(--primary)) 15%, transparent)` | `color-mix(in oklab, oklch(var(--primary)) 20%, transparent)` |
| `--primary-soft-hover` | `color-mix(in oklab, oklch(var(--primary)) 20%, transparent)` | `color-mix(in oklab, oklch(var(--primary)) 28%, transparent)` |
| `--ring` | `44% 0.017 286` | `55% 0.016 286` |
| `--secondary` | `97% 0.003 265` | `27% 0.006 286` |
| `--secondary-foreground` | `21% 0.006 286` | `97% 0.003 265` |
| `--success` | `72.05% 0.192 149.5` | `79.99% 0.182 151.8` |
| `--success-foreground` | `100% 0 0` | `38.98% 0.089 152.7` |
| `--success-hover` | `color-mix(in oklab, oklch(var(--success)) 90%, oklch(var(--success-foreground)) 10%)` | `color-mix(in oklab, oklch(var(--success)) 90%, oklch(var(--success-foreground)) 10%)` |
| `--warning` | `76.97% 0.165 70.6` | `86.11% 0.173 92.0` |
| `--warning-foreground` | `0% 0 0` | `42.36% 0.091 56.8` |
| `--warning-hover` | `color-mix(in oklab, oklch(var(--warning)) 90%, oklch(var(--warning-foreground)) 10%)` | `color-mix(in oklab, oklch(var(--warning)) 90%, oklch(var(--warning-foreground)) 10%)` |
## Surfaces
Surface and overlay tokens that create depth hierarchy in the UI.
| Token | Light | Dark |
| ---------------------------- | -------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- |
| `--card` | `98.48% 0 0` | `16.30% 0.006 285.7` |
| `--card-foreground` | `14.05% 0.004 285.8` | `98.48% 0 0` |
| `--field-background` | `100% 0 0` | `16.30% 0.006 285.7` |
| `--field-border` | `oklch(0% 0 0 / 0.12)` | `oklch(100% 0 0 / 0.12)` |
| `--field-border-focus` | `oklch(0% 0 0 / 0.35)` | `oklch(100% 0 0 / 0.35)` |
| `--field-border-hover` | `oklch(0% 0 0 / 0.22)` | `oklch(100% 0 0 / 0.22)` |
| `--field-border-invalid` | `oklch(var(--destructive))` | `oklch(var(--destructive))` |
| `--machine-bg` | `2.8% 0 0` | `2.8% 0 0` |
| `--machine-block-gap` | `1rem` | `1rem` |
| `--machine-border` | `100% 0 0 / 0.22` | `100% 0 0 / 0.22` |
| `--machine-code-size` | `0.84rem` | `0.84rem` |
| `--machine-content-max` | `76ch` | `76ch` |
| `--machine-fg` | `98% 0 0` | `98% 0 0` |
| `--machine-fg-muted` | `88% 0 0` | `88% 0 0` |
| `--machine-fg-subtle` | `72% 0 0` | `72% 0 0` |
| `--machine-font-size-base` | `0.95rem` | `0.95rem` |
| `--machine-heading-weight` | `640` | `640` |
| `--machine-inverse` | `96% 0 0` | `96% 0 0` |
| `--machine-inverse-fg` | `8% 0 0` | `8% 0 0` |
| `--machine-line-height-base` | `1.7` | `1.7` |
| `--machine-panel` | `4.4% 0 0` | `4.4% 0 0` |
| `--machine-panel-raised` | `7.2% 0 0` | `7.2% 0 0` |
| `--machine-rail-offset` | `clamp(0.9rem, 2vw, 1.45rem)` | `clamp(0.9rem, 2vw, 1.45rem)` |
| `--machine-rule` | `100% 0 0 / 0.3` | `100% 0 0 / 0.3` |
| `--machine-section-gap` | `2.5rem` | `2.5rem` |
| `--overlay` | `100% 0 0` | `19.50% 0.008 285.7` |
| `--overlay-backdrop` | `oklch(0% 0 0 / 0.40)` | `oklch(0% 0 0 / 0.55)` |
| `--overlay-foreground` | `14.05% 0.004 285.8` | `98.48% 0 0` |
| `--popover` | `100% 0 0` | `14.05% 0.004 285.8` |
| `--popover-foreground` | `14.05% 0.004 285.8` | `98.48% 0 0` |
| `--surface` | `98.48% 0 0` | `16.30% 0.006 285.7` |
| `--surface-foreground` | `14.05% 0.004 285.8` | `98.48% 0 0` |
| `--surface-secondary` | `color-mix(in oklab, oklch(var(--surface)) 94%, oklch(var(--surface-foreground)) 6%)` | `color-mix(in oklab, oklch(var(--surface)) 94%, oklch(var(--surface-foreground)) 6%)` |
| `--surface-tertiary` | `color-mix(in oklab, oklch(var(--surface)) 88%, oklch(var(--surface-foreground)) 12%)` | `color-mix(in oklab, oklch(var(--surface)) 88%, oklch(var(--surface-foreground)) 12%)` |
## Shadows
Shadow tokens with 3 semantic tiers, adaptive for dark mode.
| Token | Light | Dark |
| ---------------------- | ---------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------ |
| `--shadow-field` | `0 1px 2px 0 oklch(0% 0 0 / 0.05), 0 0 0 1px oklch(0% 0 0 / 0.04)` | `none` |
| `--shadow-inset-track` | `inset 0 1px 2px oklch(0% 0 0 / 0.06), inset 0 0 0 1px oklch(0% 0 0 / 0.04)` | `inset 0 1px 2px oklch(0% 0 0 / 0.12), inset 0 0 0 1px oklch(100% 0 0 / 0.06)` |
| `--shadow-overlay` | `0 8px 30px oklch(0% 0 0 / 0.12), 0 2px 8px oklch(0% 0 0 / 0.06), 0 0 0 1px oklch(0% 0 0 / 0.06)` | `0 0 0 1px oklch(100% 0 0 / 0.08), 0 8px 30px oklch(0% 0 0 / 0.35)` |
| `--shadow-surface` | `0 2px 4px 0 oklch(0% 0 0 / 0.04), 0 1px 2px 0 oklch(0% 0 0 / 0.06), 0 0 0 1px oklch(0% 0 0 / 0.04)` | `none` |
| `--shadow-tooltip` | `0 2px 8px oklch(0% 0 0 / 0.12), 0 1px 3px oklch(0% 0 0 / 0.08)` | `0 0 0 1px oklch(100% 0 0 / 0.10), 0 4px 16px oklch(0% 0 0 / 0.4)` |
## Radius
Border radius scale derived from the base --radius token.
| Token | Light | Dark |
| -------------- | ---------------------------- | ---- |
| `--radius` | `0.5rem` | — |
| `--radius-2xl` | `calc(var(--radius) + 8px)` | — |
| `--radius-3xl` | `calc(var(--radius) + 12px)` | — |
| `--radius-lg` | `var(--radius)` | — |
| `--radius-md` | `calc(var(--radius) - 2px)` | — |
| `--radius-sm` | `calc(var(--radius) - 4px)` | — |
| `--radius-xl` | `calc(var(--radius) + 4px)` | — |
## Typography
Font families and type scale tokens.
| Token | Light | Dark |
| -------------------------- | -------------------------------- | ---- |
| `--font-heading` | `var(--font-overpass)` | — |
| `--font-mono` | `var(--font-geist-mono)` | — |
| `--font-pixel` | `var(--font-geist-pixel-square)` | — |
| `--font-sans` | `var(--font-geist-sans)` | — |
| `--text-2xl` | `1.5rem` | — |
| `--text-2xl--line-height` | `2rem` | — |
| `--text-3xl` | `1.875rem` | — |
| `--text-3xl--line-height` | `2.25rem` | — |
| `--text-base` | `1rem` | — |
| `--text-base--line-height` | `1.5rem` | — |
| `--text-lg` | `1.125rem` | — |
| `--text-lg--line-height` | `1.75rem` | — |
| `--text-sm` | `0.875rem` | — |
| `--text-sm--line-height` | `1.25rem` | — |
| `--text-xl` | `1.25rem` | — |
| `--text-xl--line-height` | `1.75rem` | — |
| `--text-xs` | `0.75rem` | — |
| `--text-xs--line-height` | `1rem` | — |
## Spacing
Base spacing unit for the design system.
| Token | Light | Dark |
| ----------- | --------- | ---- |
| `--spacing` | `0.25rem` | — |
## Animation
Easing curves and keyframe animation tokens.
| Token | Light | Dark |
| ---------------------------------- | -------------------------------------------------- | ---- |
| `--animate-accordion-down` | `accordion-down 0.2s ease-out` | — |
| `--animate-accordion-up` | `accordion-up 0.2s ease-out` | — |
| `--animate-progress-indeterminate` | `progress-indeterminate 1.8s ease-in-out infinite` | — |
| `--ease-in-out-quad` | `cubic-bezier(0.455, 0.03, 0.515, 0.955)` | — |
| `--ease-in-quad` | `cubic-bezier(0.55, 0.085, 0.68, 0.53)` | — |
| `--ease-in-quart` | `cubic-bezier(0.895, 0.03, 0.685, 0.22)` | — |
| `--ease-out-fluid` | `cubic-bezier(0.32, 0.72, 0, 1)` | — |
| `--ease-out-quad` | `cubic-bezier(0.25, 0.46, 0.45, 0.94)` | — |
| `--ease-out-quart` | `cubic-bezier(0.165, 0.84, 0.44, 1)` | — |
| `--ease-smooth` | `cubic-bezier(0.4, 0, 0.2, 1)` | — |