Custom Primitives
A custom primitive is a self-contained React component registered with the IKARY runtime. Manifests reference primitives by key; the runtime renders the matching component with the resolved props.
Custom primitives extend the built-in set with project-specific UI. They follow the same contract format as core primitives and appear in the Primitive Studio alongside them.
Anatomy
Every primitive lives in its own folder with six files:
primitives/<name>/
<Name>.tsx React component, typed with the Zod schema
<Name>PresentationSchema.ts Zod schema — source of truth for prop types
<name>.contract.yaml Human-readable props contract
<Name>.resolver.ts Transforms contract props to resolved props
<Name>.register.ts Registers the component with the runtime
<Name>.example.ts Named example scenarios for the StudioAll six files are generated by ikary primitive add. You implement the component and Zod schema; the other files need minimal changes for most primitives.
Scaffolding a primitive
ikary primitive add my-widgetThe command prompts for a label, description, and category:
Add primitive
? Display label › My Widget
? Short description › A custom widget primitive
? Category › custom
✔ Created my-widget primitive
primitives/my-widget/MyWidget.tsx React component
primitives/my-widget/MyWidgetPresentationSchema.ts Zod props schema
primitives/my-widget/my-widget.contract.yaml Human-readable contract
primitives/my-widget/MyWidget.resolver.ts Props resolver
primitives/my-widget/MyWidget.register.ts Registration
primitives/my-widget/MyWidget.example.ts Example scenarios
ikary-primitives.yaml UpdatedImplementing the component
Edit two files:
<Name>PresentationSchema.ts — add your props:
export const MyWidgetPresentationSchema = z.object({
title: z.string(),
count: z.number().default(0),
}).strict();
export type MyWidgetProps = z.infer<typeof MyWidgetPresentationSchema>;<Name>.tsx — implement the component:
import type { MyWidgetProps } from './MyWidgetPresentationSchema';
export function MyWidget({ title, count }: MyWidgetProps) {
return (
<div>
<h2>{title}</h2>
<span>{count}</span>
</div>
);
}Update <name>.contract.yaml to match — this drives the Studio props editor:
key: my-widget
version: "1.0.0"
label: My Widget
category: custom
props:
type: object
properties:
title:
type: string
description: Widget heading
count:
type: number
description: Display count
required: [title]Previewing in the Studio
Start the local stack and open the Primitive Studio:
ikary local start manifest.json
# then open in your browser:
# http://localhost:4500/__primitive-studioThe Studio shows all custom primitives registered in ikary-primitives.yaml. Select a primitive to see:
- Left panel — list of custom primitives grouped by category
- Center panel — scenario tabs and an editable props JSON editor
- Right panel — live component preview that updates as you edit props
Add scenarios in <Name>.example.ts:
export const MyWidgetExamples = [
{
label: 'Default',
description: 'Basic example',
props: { title: 'Hello', count: 0 },
},
{
label: 'With count',
props: { title: 'Items', count: 42 },
},
];Validating the contract
ikary primitive validateThis checks every entry in ikary-primitives.yaml: the contract YAML parses against the schema, the source file exists, and referenced example props match the declared contract.
ikary-primitives.yaml
The config file at the project root lists all registered custom primitives:
apiVersion: ikary.co/v1alpha1
kind: PrimitiveConfig
primitives:
- key: my-widget
version: "1.0.0"
source: ./primitives/my-widget/MyWidget.register.ts
contract: ./primitives/my-widget/my-widget.contract.yaml
examples: ./primitives/my-widget/MyWidget.example.tsikary primitive add appends to this file automatically. Edit it manually to adjust paths or add an overrides field to replace a core primitive.
Declaring slots
Primitives that act as containers can expose named zones for further injection. Declare them in the .contract.yaml under a slots key:
key: my-layout
version: "1.0.0"
label: My Layout
category: layout
props:
type: object
properties:
title:
type: string
description: Layout title
required: [title]
slots:
- name: header
description: Top area of the layout
allowedModes: [replace, prepend, append]
- name: body
description: Main content area
allowedModes: [replace, append]Each slot entry requires name. The description field is optional but appears in the MCP tool output. The allowedModes field restricts which binding modes are valid. Omit it to allow all three: replace, prepend, append.
Once declared, a manifest can target these slots using slotBindings on any page that renders the primitive.
entityBinding
If your primitive is designed for a specific entity type, set entityBinding in ikary-primitives.yaml:
primitives:
- key: product-summary
version: "1.0.0"
source: ./primitives/product-summary/ProductSummary.register.ts
contract: ./primitives/product-summary/product-summary.contract.yaml
entityBinding: productSet it to an array to allow multiple entity types:
entityBinding: [product, variant]This is metadata only. The runtime does not block bindings that do not match. The validate_slot_bindings MCP tool uses it to generate warnings when a primitive is bound to a page whose entity differs.
Building with Claude Code
Run ikary setup ai once to configure Claude Code for the project. After that, use these slash commands inside Claude Code:
| Command | What it does |
|---|---|
/ikary-create-primitive | Scaffolds a new primitive and implements the component based on your description |
/ikary-update-primitive | Updates an existing primitive, with guidance on breaking versus non-breaking changes |
/ikary-browse-primitives | Lists all custom primitives and shows their contracts and example props |
Example session:
ikary primitive add my-widget
cd my-project
claude
> /ikary-create-primitive
> build the my-widget primitive — it should display a count with an iconClaude reads the scaffolded files, implements the component, updates the Zod schema and contract, runs ikary primitive validate, and prints the Studio URL.